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 `![name](filename)` 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: `![Summit](col-summit.jpg)`. -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} + -

{activity.title}

- {#if detail?.description} -

{detail.description}

+
+

{displayTitle}

+ {#if editUrl} + + {/if} +
+ {#if descriptionHtml} +
+ {@html descriptionHtml} +
{/if} diff --git a/site/src/components/EditDrawer.svelte b/site/src/components/EditDrawer.svelte new file mode 100644 index 0000000..259f83a --- /dev/null +++ b/site/src/components/EditDrawer.svelte @@ -0,0 +1,291 @@ + + + +
dispatch('saved', { title, description })} + role="presentation" +/> + + +