Files

130 lines
5.0 KiB
Markdown

# 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.