Skip to content

feat(api): station journal backend — entries, photos, pins#641

Closed
themightychris wants to merge 6 commits into
mainfrom
feat/station-journal-entries
Closed

feat(api): station journal backend — entries, photos, pins#641
themightychris wants to merge 6 commits into
mainfrom
feat/station-journal-entries

Conversation

@themightychris

@themightychris themightychris commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Full backend for the station journal (companion contract: pathways-field-companion specs/api/station-journal.md). A journal entry is a field-collected note and/or one or more photos, optionally targeting the station, a node, a pathway, or a pin (an arbitrary point on a level). Internal to Studio; never GTFS-exported.

Entries

  • journal_entries table + schema: client-generated id (upsert key), org/version/station_id scoping, polymorphic target_type + target_id, body, author_id, captured_at, resolved_at.
  • Sync: POST .../sync also upserts a journal_entries[] array (per-item errors, partial success stays 200, journal_synced_count); rejects entries for a station outside the caller's org.
  • Bundle: station entries top-level, node/pathway entries on their objects.

Photos

  • journal_photos table; binary stored under /uploads/field-captures/... and served as a static URL (mirrors floorplans).
  • POST .../journal-photos multipart upload — idempotent on id, content-type allow-list (jpg/png), 25 MB cap, entry+station+org scoping.
  • Bundle nests each entry's photos[] with absolute URLs.

Pins (diagram-anchored, like nodes)

  • A pin's canonical coordinate is its diagram_coordinate — identical to how nodes are anchored — so pins work on every station regardless of geo-alignment (most stations have no lat/lon).
  • lat/lon is optional enrichment imputed at level-alignment time alongside node coordinates (save_and_apply_stop_level_alignment re-imputes the level's pins in the same transaction), never at sync. Re-syncing a pin preserves its imputed coords.
  • Bundle serves pins on levels[].journal_entries with diagram_coordinate (+ lat/lon when aligned), identical in shape to node coordinates.

Auth / scoping

Session-backed tokens (not raw JWT claims): author_idcurrent_user_id, org ← current_organization_id.

Tests & checks

New ExUnit coverage for entries (create/idempotent/validation/cross-org), photos (upload/idempotent/404/422/bundle nesting), and pins (diagram-space storage, alignment-time imputation, unaligned→null, re-sync preserves coords). Full suite green: 1929 tests, 0 failures. mix format clean; mix credo clean on new code.

Notes

  • First commit is a chore(format) of pre-existing unformatted files (already failing mix format --check-formatted on main) so this PR's CI is green and the feature diff stays clean.
  • Targets main (no develop here). The companion spec should pick up two small follow-ups: add station_id to the data model, and note pin lat/lon is imputed at alignment time (not sync) + the session-token auth wording.

🤖 Generated with Claude Code

themightychris and others added 2 commits June 22, 2026 20:47
These were already unformatted on main and fail `mix format --check-formatted`
in CI; formatting them here so the station-journal PR's CI is green. No
behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
…alization

Backend PR A of the station journal (companion specs/api/station-journal.md):
the journal_entries table + the entries half of the contract. Photos and pin
targets follow in later PRs.

- Migration + JournalEntry schema: client-generated id (upsert key), org/version
  scoping, station_id, polymorphic target (station|node|pathway; pin later),
  body, author_id, captured_at, resolved_at. Author as a plain id (change_logs
  precedent). Target_id validity is a changeset rule.
- Context: list_journal_entries_for_station/3 and upsert_journal_entry/1
  (idempotent on id; LWW on body/resolved_at).
- Sync: SyncController.create/2 also upserts a journal_entries[] array, scoped to
  the path's version/station + the session's org/user, with per-item errors
  (partial success stays 200, like the pathway loop) and journal_synced_count.
  Entries for a station outside the org are rejected.
- Bundle: station-targeted entries top-level, node/pathway entries on their
  stop/pathway, each nesting photos: [] (populated once photos land).

Tests: bundle serialization across station/node/pathway targets; sync create,
idempotent upsert, per-item validation error, and cross-org rejection. Full
suite green (1919 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
@themightychris
themightychris force-pushed the feat/station-journal-entries branch from e76925b to d509179 Compare June 23, 2026 00:57
Completes the station-journal backend (PRs B + C) on top of journal entries.

Photos (B):
- journal_photos table + JournalPhoto schema; binary stored under
  /uploads/field-captures/<org>/<station_dir>/ and served as a static URL
  (mirrors floorplans).
- POST .../journal-photos multipart upload (idempotent on id; content-type
  allow-list jpg/png; 25 MB cap; org/version/station + entry scoping).
- Bundle nests each entry's photos[] with absolute URLs.

Pins (C):
- A pin is a journal entry anchored to an arbitrary point on a level — its
  canonical coordinate is the diagram_coordinate, exactly like a node. Pins
  work on every station regardless of geo-alignment (most aren't aligned).
- lat/lon is optional enrichment imputed at level-alignment time alongside
  node coordinates (save_and_apply_stop_level_alignment re-imputes the level's
  pins in the same transaction), never at sync. Re-syncing a pin preserves its
  imputed lat/lon.
- Bundle serves pins on levels[].journal_entries with diagram_coordinate (+ lat/lon
  when aligned), identical in shape to how nodes carry coordinates.

Tests: photo upload (success/idempotent/404/422/bundle nesting); pin sync stores
diagram coords with null lat/lon; alignment imputes pin lat/lon alongside nodes;
unaligned levels keep null; re-sync preserves imputed coords. Full suite green
(1929 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
@themightychris themightychris changed the title feat(api): station journal entries (backend PR A) feat(api): station journal backend — entries, photos, pins Jun 23, 2026
themightychris and others added 3 commits June 23, 2026 09:41
The companion needs a level's StopLevel UUID to anchor a newly-dropped pin
journal entry to that level. serialize_level emitted only the Level id; pin
entries echo stop_level_id back, but a fresh drop needs it from the level.
Add stop_level_id (null when the level has no stop_level association).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
Rename the open/closed workflow field resolved_at → closed_at (null = open) and
add closed_by (the user who closed it, a plain id like author_id). Closing is a
desk/Studio action; the contract just carries the fields. Applied across the
create_journal_entries migration, the JournalEntry schema, the sync upsert (+
on_conflict replace list), and the bundle serializers; round-trip test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
…upload

The journal-photo upload dropped width/height — they were never read from the
multipart metadata, so every photo stored null dimensions. Read width/height
from metadata and pass them through upsert_journal_photo (the schema already
casts them and the on_conflict replaces them). The server doesn't decode
uploads; dimensions are client-computed at capture. Absent for older clients →
null, unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
@themightychris

Copy link
Copy Markdown
Collaborator Author

Superseded by #655 (merged), which reimplemented this backend while retaining the companion API contract. The desktop review surface (#643) will be rebased onto main's merged implementation.

themightychris added a commit that referenced this pull request Jul 14, 2026
Surface companion-captured journal entries in Pathways Studio so they can be
reviewed at a desk and closed. Adds to the station diagram LiveView:

- A "Journal" toolbar button (open-entry count badge) opening a drawer that lists
  entries newest-first with target chip (station/node/pathway/pin), author, time,
  note, and photo thumbnails (full image opens in a new tab).
- Close / reopen as a desk action: close_journal_entry/2 + reopen_journal_entry/1
  context fns set/clear closed_at + closed_by. Closed entries stay in the list,
  de-emphasized with a "Closed by X · when" badge — never hidden or filtered out.
- Pin markers on the level diagram overlay, drawn from diagram_x/diagram_y in the
  same 0–100 space as node markers, filtered to the active level by stop_level_id;
  clicking one opens the drawer focused on that entry.

Photo URLs reuse the field-capture path; author/closer names resolve to emails.
LiveView tests cover listing, close (stays visible), reopen, the pin marker, and
the count badge. Stacks on the journal backend (#641) until it merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UyezV5kznW5rbRFVsoXWvh
themightychris added a commit that referenced this pull request Jul 14, 2026
The station-journal backend merged to main via #655 (replacing #641, which
this branch was stacked on). Same tables and wire contract, different domain
shape — journal operations now go through GtfsPlanner.Gtfs.StationJournal
with a trusted Scope struct:

- load_journal builds a Scope from the LiveView's already-authorized
  org/version/station + current user and calls Gtfs.list_station_journal/1;
  photos arrive preloaded on each entry (the separate photo listing and
  group_by are gone).
- Photo thumbnails use PhotoStorage.public_path/2 instead of a hand-rolled
  field-captures URL helper.
- switch_to_level drops assign_selectable_reference_stop_levels/
  sync_reference_overlay_state — main replaced the reference-overlay
  internals with the other-levels work (#646); the journal pipeline mirrors
  main's search_stop level switch.
- Test fixtures insert JournalEntry rows directly (the 641-era
  upsert_journal_entry sync path no longer exists; the companion's write
  path is the scoped sync API, which these desktop read/close tests don't
  exercise).

Full suite on this branch: 2012 tests, 0 failures, 6 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoGkr6FT6SKZCYDBkNqKBj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant