Add MkDocs documentation: architecture, deployment, development, decisions
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# Edit Sidecar
|
||||
|
||||
The edit sidecar (`edit/server.py`) is a FastAPI application that handles all write operations. It runs alongside the static Astro build and is the only process that modifies content on disk.
|
||||
|
||||
## API endpoints
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
| `GET` | `/api/me` | session | Return current user info |
|
||||
| `POST` | `/api/auth/login` | — | Create session, set cookie |
|
||||
| `POST` | `/api/auth/logout` | session | Destroy session, clear cookie |
|
||||
| `POST` | `/api/auth/register` | — | Register via invite token |
|
||||
|
||||
### Wiki pages
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/pages` | List all page slugs |
|
||||
| `GET` | `/pages/{slug}` | Get page content + `base_hash` |
|
||||
| `POST` | `/pages/{slug}` | Save page (with 3-way merge) |
|
||||
| `DELETE` | `/pages/{slug}` | Delete page |
|
||||
|
||||
### Blog / stories
|
||||
|
||||
Same as pages, with `/stories` prefix.
|
||||
|
||||
### Invites
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/invites` | List own invites |
|
||||
| `POST` | `/api/invites` | Create a new wiki invite |
|
||||
| `DELETE` | `/api/invites/{code}` | Revoke an unused invite |
|
||||
|
||||
### Other
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `POST` | `/api/assets` | Upload image (multipart) |
|
||||
| `POST` | `/rebuild` | Trigger Astro rebuild + rsync |
|
||||
| `GET` | `/api/log` | Last 50 web-editor commits |
|
||||
|
||||
## Git attribution
|
||||
|
||||
Every page save or delete is committed to git with the editor's handle as the author:
|
||||
|
||||
```
|
||||
git add pages/bincio-tech/gps-e-ciclocomputer.md
|
||||
git commit -m "brut: edited gps-e-ciclocomputer" \
|
||||
--author="brut <brut@bincio.wiki>"
|
||||
git update-ref refs/heads/main HEAD
|
||||
```
|
||||
|
||||
The committer is always `bincio-wiki <wiki@bincio.wiki>`. The author field carries the human attribution.
|
||||
|
||||
`GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` are set in the environment passed to each subprocess. On the VPS, `GIT_DIR` points to the bare repo at `/opt/bincio-wiki-repo.git` and `GIT_WORK_TREE` to `/opt/bincio_wiki`.
|
||||
|
||||
### The `update-ref` fix
|
||||
|
||||
The VPS post-receive hook runs `git checkout -f <SHA>` to check out the new tree. This detaches `HEAD` in the bare repo: subsequent sidecar commits advance the detached HEAD pointer but not `refs/heads/main`. On the next `git push` from a developer's machine, those sidecar commits become unreachable and are eventually garbage-collected.
|
||||
|
||||
The fix: after every `git commit`, the sidecar immediately runs:
|
||||
|
||||
```
|
||||
git update-ref refs/heads/main HEAD
|
||||
```
|
||||
|
||||
This keeps `refs/heads/main` in sync with the detached HEAD, so sidecar commits are always reachable via the branch and survive a future push.
|
||||
|
||||
### Serialisation
|
||||
|
||||
All git operations are serialised with a module-level `asyncio.Lock()`. This prevents two concurrent saves from interleaving `git add` + `git commit` calls and producing a corrupted index or commit history.
|
||||
|
||||
## Optimistic concurrency
|
||||
|
||||
When a page is opened for editing, the sidecar returns the current HEAD commit hash alongside the content:
|
||||
|
||||
```json
|
||||
{ "slug": "gps-e-ciclocomputer", "content": "...", "base_hash": "a85a2ee" }
|
||||
```
|
||||
|
||||
The editor holds this hash. On save, it sends it back:
|
||||
|
||||
```json
|
||||
{ "content": "...", "base_hash": "a85a2ee" }
|
||||
```
|
||||
|
||||
The sidecar compares `base_hash` against the current HEAD for that file. If they differ, another edit landed while the user was writing.
|
||||
|
||||
## 3-way merge
|
||||
|
||||
Rather than rejecting the save with a 409, the sidecar attempts a 3-way merge using `git merge-file`:
|
||||
|
||||
```
|
||||
base = content at base_hash (what the user started with)
|
||||
current = content at HEAD (what's on disk now)
|
||||
user = content from request (what the user wrote)
|
||||
```
|
||||
|
||||
If the edits touched different lines, `git merge-file` produces a clean merged result and the save proceeds transparently. If the same lines were changed by both, `git merge-file` exits with a non-zero code and embeds conflict markers (`<<<<<<< attuale / ======= / >>>>>>> handle`). The sidecar returns a 409 with the conflict-marked content so the user can resolve manually.
|
||||
|
||||
```python
|
||||
# exit code 0 → clean merge; > 0 → conflicts present
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "merge-file",
|
||||
"-L", "attuale", "-L", "base", "-L", handle,
|
||||
str(p_current), str(p_base), str(p_user),
|
||||
...
|
||||
)
|
||||
await proc.wait()
|
||||
merged_content = p_current.read_text()
|
||||
has_conflict = proc.returncode > 0
|
||||
```
|
||||
|
||||
## Rebuild
|
||||
|
||||
`POST /rebuild` does three things:
|
||||
|
||||
1. Deletes `.astro/data-store.json` — Astro's persisted content cache. Without this, changes to files in `pages/` and `blog/` (which live outside `site/`) are not picked up by a cached build.
|
||||
2. Runs `npm run build -- --force` in `site/`. The `--force` flag tells Astro to ignore any remaining stale cache.
|
||||
3. If `WIKI_WEBROOT` is set, rsyncs `site/dist/` to the webroot so nginx serves the updated build immediately.
|
||||
|
||||
The rebuild is triggered automatically by the `PageEditor.svelte` after every successful save.
|
||||
|
||||
## Slug validation
|
||||
|
||||
Slugs are validated against `^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$` and checked for path traversal before any file operation. The resolved path must be a child of the configured base directory.
|
||||
Reference in New Issue
Block a user