feat(api): station journal backend — entries, photos, pins#641
Closed
themightychris wants to merge 6 commits into
Closed
feat(api): station journal backend — entries, photos, pins#641themightychris wants to merge 6 commits into
themightychris wants to merge 6 commits into
Conversation
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
force-pushed
the
feat/station-journal-entries
branch
from
June 23, 2026 00:57
e76925b to
d509179
Compare
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
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
Collaborator
Author
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full backend for the station journal (companion contract:
pathways-field-companionspecs/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_entriestable + schema: client-generatedid(upsert key), org/version/station_idscoping, polymorphictarget_type+target_id,body,author_id,captured_at,resolved_at.POST .../syncalso upserts ajournal_entries[]array (per-item errors, partial success stays 200,journal_synced_count); rejects entries for a station outside the caller's org.Photos
journal_photostable; binary stored under/uploads/field-captures/...and served as a static URL (mirrors floorplans).POST .../journal-photosmultipart upload — idempotent onid, content-type allow-list (jpg/png), 25 MB cap, entry+station+org scoping.photos[]with absolute URLs.Pins (diagram-anchored, like nodes)
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/lonis optional enrichment imputed at level-alignment time alongside node coordinates (save_and_apply_stop_level_alignmentre-imputes the level's pins in the same transaction), never at sync. Re-syncing a pin preserves its imputed coords.levels[].journal_entrieswithdiagram_coordinate(+ lat/lon when aligned), identical in shape to node coordinates.Auth / scoping
Session-backed tokens (not raw JWT claims):
author_id←current_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 formatclean;mix credoclean on new code.Notes
chore(format)of pre-existing unformatted files (already failingmix format --check-formattedon main) so this PR's CI is green and the feature diff stays clean.main(nodevelophere). The companion spec should pick up two small follow-ups: addstation_idto the data model, and note pin lat/lon is imputed at alignment time (not sync) + the session-token auth wording.🤖 Generated with Claude Code