Files

5.0 KiB

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:

{ "slug": "gps-e-ciclocomputer", "content": "...", "base_hash": "a85a2ee" }

The editor holds this hash. On save, it sends it back:

{ "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.

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