diff --git a/CLAUDE.md b/CLAUDE.md
index 88eca60..dbaedc1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -199,44 +199,46 @@ Sidecars work for *remote* activities too: if you include someone else's BAS fee
you can write local `.md` sidecars for their activity IDs. Your render stage applies
your overrides on top of their data. This is a natural extension of the local case.
-### Editing UX: `bincio edit --serve`
+### Editing UX: drawer in Astro + `bincio edit` write API
-A separate FastAPI server (`bincio edit --serve`, default port 4041) handles all writes.
-The static site and Astro are untouched — no hybrid mode, no dead-code API routes in prod.
+The edit UI is a **slide-in drawer** (`EditDrawer.svelte`) in the Astro site.
+The drawer fetches from and POSTs to the `bincio edit` FastAPI server (write API only —
+the server no longer serves its own HTML UI).
**How it works:**
```
-bincio edit --serve --data ~/bincio_data # starts on :4041
+bincio render --serve # Astro dev server, port 4321
+bincio edit --data-dir ~/… # write API only, port 4041
```
-- Serves a bundled Svelte UI (single compiled HTML, reuses existing Svelte investment)
-- `GET /api/activity/{id}` — returns merged BAS JSON + existing sidecar fields
-- `POST /api/activity/{id}` — writes `edits/{id}.md` to the data dir
+- Edit button appears on the activity detail page **only when `PUBLIC_EDIT_URL` is set** in `site/.env`
+- Clicking Edit opens the drawer in the same page — no navigation, no copy-pasting IDs
+- Drawer fetches `GET /api/activity/{id}` to pre-fill, `POST /api/activity/{id}` to save
+- After save: server runs `merge_all()` automatically → Astro serves updated data immediately on refresh
+- Closing the drawer applies `title` + `description` changes optimistically to the local page state
+ (no full reload required to see the text change)
+
+**`PUBLIC_EDIT_URL` as feature flag:**
+- **Unset** → no Edit button, no drawer. Works as a normal static site. Safe for public hosting.
+- **Set** (e.g. `http://localhost:4041`) → editing enabled. Lives in `site/.env` (gitignored).
+ Each deployment opts in explicitly.
+
+**Edit server API (`bincio edit --data-dir
`):**
+- `GET /api/activity/{id}` — current values (sidecar overrides layered on BAS JSON)
+- `POST /api/activity/{id}` — write sidecar `.md`, trigger `merge_all()`
- `POST /api/activity/{id}/images` — multipart upload → `edits/images/{id}/{filename}`
-- The Astro dev server's file watcher picks up `.md` writes → incremental rebuild
+- `DELETE /api/activity/{id}/images/{filename}` — remove uploaded image
-**Edit UI features:**
-- Title text input (pre-filled from BAS JSON)
-- Sport dropdown (pre-filled, shows all known sport types)
-- Markdown textarea for description, with minimal toolbar (bold, italic, link, image insert)
-- Live markdown preview panel
-- `hide_stats` checkbox group: elevation, speed, heart_rate, cadence, power
-- `highlight` toggle (feature in feed)
-- `private` toggle (suppress from feed at render time)
-- Image drag-and-drop zone → uploads to `edits/images/{id}/`, inserts `![]()` into textarea
-- Save button → POST to API → success toast
+**Edit drawer features:**
+- Title, sport dropdown, gear
+- Markdown textarea for description (images inserted as `` references)
+- Image drag-and-drop zone with chip list + delete
+- Hide stat panels (elevation, speed, heart_rate, cadence, power) — toggle buttons
+- Highlight flag (★ — sorts to top of feed, visual badge)
+- Private flag (⊘ — suppressed from index at render time)
-**Workflow (typical):**
-1. User browses the Astro dev server on :4040
-2. Activity detail page has an "Edit" button (rendered only when `PUBLIC_EDIT_URL` env var is set)
-3. Button links to `:4041/edit/{id}` — opens the FastAPI-served edit UI
-4. User fills in form, saves → sidecar written → Astro rebuilds → refreshing :4040 shows changes
-
-The `PUBLIC_EDIT_URL` env var in `.env` controls whether the Edit button appears;
-leave it unset for production builds, set to `http://localhost:4041` for local dev.
-
-### Image storage
+### Image storage and serving
```
~/bincio_data/
@@ -249,17 +251,20 @@ leave it unset for production builds, set to `http://localhost:4041` for local d
```
Images are referenced in the markdown body with relative paths: ``.
-The render stage resolves relative image paths against `edits/images/{id}/` and copies them
-to `site/public/images/activities/{id}/` so they're served from the static site.
+`merge_all()` symlinks `edits/images/{id}/` → `_merged/activities/images/{id}/` so images
+are served at `data/activities/images/{id}/{filename}` by the Astro dev server.
+`ActivityDetail.svelte` rewrites relative image paths to this URL when rendering markdown.
+
+**Note:** browsers cannot display `.HEIC` files. Convert to JPEG/PNG first:
+`sips -s format jpeg photo.HEIC --out photo.jpg` (macOS).
### Decided
-- **Sidecar location**: `edits/` subdirectory (not co-located with JSON) — cleaner, easier to
- backup/sync just your customisations independently of the extracted data
-- **`private: true`**: suppresses from `index.json` at render time (not client-side hide) —
- safer for public hosting
-- **`highlight`**: visual badge in feed + sorted before non-highlighted activities
-- **Edit UI**: `bincio edit --serve` FastAPI server (Option B) — not integrated into Astro
+- **Sidecar location**: `edits/` subdirectory — cleaner, easier to backup/sync independently
+- **Merge output**: `data/_merged/` — extracted data stays pristine; `public/data` → `_merged/`
+- **`private: true`**: suppressed from `index.json` at render time (not client-side hide)
+- **`highlight`**: sorts to top of feed; visual badge TBD
+- **Edit UI**: drawer in Astro site, `bincio edit` is a pure write API (no HTML serving)
## Known issues / next steps
@@ -282,9 +287,12 @@ to `site/public/images/activities/{id}/` so they're served from the static site.
- [ ] Map thumbnail in activity cards (SVG path from GeoJSON)
- [ ] GitHub Actions template for auto-publish
- [ ] Karoo/Garmin Connect importers beyond Strava
-- [ ] `bincio.render.merge` module: walk `edits/`, parse sidecars, produce enriched data for Astro
-- [ ] `bincio render --watch` incremental rebuild on sidecar changes
-- [ ] Sidecar `.md` format: title, sport, description, hide_stats, highlight, private, images
-- [ ] `bincio edit --serve` FastAPI server with Svelte edit UI (port 4041)
-- [ ] Edit button on activity detail pages (visible when `PUBLIC_EDIT_URL` env var set)
-- [ ] Image upload → `edits/images/{id}/`, render stage copies to `public/images/activities/{id}/`
+- [x] `bincio.render.merge` — sidecar parser, `_merged/` output, private filter, highlight sort
+- [x] `bincio edit` FastAPI write API (GET/POST activity, image upload/delete, triggers merge)
+- [x] `EditDrawer.svelte` — slide-in edit UI in the Astro site (no separate HTML from server)
+- [x] `PUBLIC_EDIT_URL` feature flag — unset = no edit UI, set = drawer enabled
+- [x] Markdown rendering in activity description with image path rewriting
+- [x] `hide_stats` support in activity detail stats panel
+- [ ] `bincio render --watch` incremental rebuild on sidecar/data changes
+- [ ] Highlight badge in activity feed cards
+- [ ] Image format warning (HEIC → JPEG conversion hint in the upload UI)
diff --git a/bincio/edit/server.py b/bincio/edit/server.py
index 9538c8f..452410f 100644
--- a/bincio/edit/server.py
+++ b/bincio/edit/server.py
@@ -403,6 +403,11 @@ async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONRespon
content += "\n" + description + "\n"
sidecar_path.write_text(content, encoding="utf-8")
+
+ # Re-merge so the Astro dev server immediately serves updated data
+ from bincio.render.merge import merge_all
+ merge_all(dd)
+
return JSONResponse({"ok": True, "sidecar": str(sidecar_path)})
diff --git a/site/.env b/site/.env
new file mode 100644
index 0000000..bb084dc
--- /dev/null
+++ b/site/.env
@@ -0,0 +1,2 @@
+BINCIO_DATA_DIR=/tmp/bincio_test
+PUBLIC_EDIT_URL=http://localhost:4041
diff --git a/site/package.json b/site/package.json
index c0f6980..6aa64c2 100644
--- a/site/package.json
+++ b/site/package.json
@@ -12,9 +12,10 @@
"dependencies": {
"@astrojs/svelte": "^7.0.0",
"@astrojs/tailwind": "^5.1.0",
+ "@observablehq/plot": "^0.6.0",
"astro": "^5.0.0",
"maplibre-gl": "^5.0.0",
- "@observablehq/plot": "^0.6.0",
+ "marked": "^17.0.5",
"svelte": "^5.0.0",
"tailwindcss": "^3.4.0"
},
diff --git a/site/src/components/ActivityDetail.svelte b/site/src/components/ActivityDetail.svelte
index 2d40ddf..40eecc5 100644
--- a/site/src/components/ActivityDetail.svelte
+++ b/site/src/components/ActivityDetail.svelte
@@ -1,17 +1,26 @@
+{#if editOpen && editUrl}
+
+{/if}
+