FotoLog v2 - agent-coded with a focus on auto-curation https://trippin.tfks.net
  • TypeScript 93.5%
  • CSS 4.5%
  • Just 1.8%
  • Dockerfile 0.1%
Find a file
Erik Stephens d80a6483de feat(observability): structured logging through a seam, reported to Marsad
Replaces the app's console.* calls with a structured logger and ships server-side records to Marsad (marsad.space). Logging is server-side only.

`src/lib/observability/log.ts` is the API everything uses — levels, fields, `child()` for binding scope, and pluggable sinks. `marsad.ts` is the only module that imports `@marsadco/sdk`, so replacing a pre-1.0, single-maintainer dependency stays a one-file change rather than a sweep through 90 call sites. `boot.ts` wires it up from env at startup. The console sink always runs and `scope` is promoted back to the familiar `[worker]` prefix, so `just dev` output reads as it did before; a sink that throws degrades to console-only rather than taking a request down.

Migrated 20 call sites: worker loop, tasks and emails, root middleware, the auth/profile/upload routes, curation fan-in, POI storage, migrations, and three island error paths. `src/scripts/` keeps console.* deliberately — there the output is the product, not telemetry.

Two decisions worth knowing. Network capture is off on the server: the SDK leaves bodies and headers off by default but still captures URLs, and in this app a URL is routinely a credential (Supabase `?token=` signed URLs, `?share=` tokens) while our outbound calls all carry auth. A `beforeSend` scrubber walks every event as a backstop. And the user is attached per-event rather than via the SDK's `setUser()`, which writes one field on one process-wide client — under concurrency that stamps request A's user onto request B's error. The root middleware builds a request-scoped `ctx.state.log` instead, reached via `logFor(ctx.state)`.

There is deliberately no browser half. The SDK targets Next.js/Expo, where `init()` runs once per session; Fresh is an MPA, so every navigation would re-parse it, re-init, and send a fresh session ping and pageview. The island measured 12 KB gzip and its per-navigation execution and ingest chatter did not earn that on a personal travel log. Rationale and the path back are in `wiki/observability.md`.

Pinned to 0.7.0: 0.8.0 is 2 days old and blocked by `minimumDependencyAge: P7D`.

`just check` clean; `just test::units` 554 passed, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-26 14:57:49 -07:00
.agents/commands/fjx update fjx agent commands 2026-06-26 18:13:50 -07:00
.claude update fjx agent commands 2026-06-26 18:13:50 -07:00
.forgejo/workflows fix(ci): the registry push uses a token that can write packages 2026-08-26 19:56:59 -07:00
.githooks chore: adopt template-deno standards 2026-05-18 12:20:47 -07:00
.vscode fix(vscode): point the Deno language server at deno.json 2026-09-13 07:16:24 -07:00
certs feat(deploy): encrypt the Supabase connection 2026-08-25 04:47:02 -07:00
coverage build(coverage): baseline is a main-only artifact, not a per-branch commit 2026-08-24 09:52:55 -07:00
openspec fix(trip-player): make the idle Play control icon-only 2026-09-13 22:37:23 -07:00
src feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
supabase fix(ci): SMTP_PORT keeps its .env override and gains a floor 2026-08-26 19:10:14 -07:00
wiki feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
.dockerignore feat(ui)!: Shoelace is removed, and its tokens become --tfks-* 2026-08-26 14:42:40 -07:00
.env.test.example chore(env,email): one env file to edit, absolute links, and mail that renders on any canvas 2026-08-24 07:15:43 -07:00
.gitignore docs(wiki): draw the line between the two issue trackers 2026-09-13 11:02:21 -07:00
.gitleaks.toml build(bakeoff): Day 0 baseline — packaging fixes and evidence pass 2026-08-25 10:22:52 -07:00
.yamllint fix(ci): semgrep stops failing the build on two known patterns 2026-08-26 19:33:27 -07:00
admin.just chore(env,email): one env file to edit, absolute links, and mail that renders on any canvas 2026-08-24 07:15:43 -07:00
AGENTS.md feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
build.just chore(just): add a build module, retire the recipes nothing calls 2026-09-15 11:50:11 -07:00
CLAUDE.md chore: adopt template-deno standards 2026-05-18 12:20:47 -07:00
deno.json feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
deno.lock feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
Dockerfile fix(docker): serve the wrapper entry, and add a health probe 2026-08-26 22:20:47 -07:00
env.example feat(observability): structured logging through a seam, reported to Marsad 2026-09-26 14:57:49 -07:00
fjx.json update fjx agent commands 2026-06-26 18:13:50 -07:00
justfile feat(pois): coordinate entry — DMS conversion, live markers, scroll-safe fields 2026-09-15 15:45:08 -07:00
LEARN.md feat(bakeoff-c): POI signals store and fetch-submit — zero page loads per edit 2026-08-25 10:42:51 -07:00
migrate.just refactor(supabase): give CI its own trimmed stack and collect the Supabase recipes 2026-08-25 04:47:02 -07:00
README.md chore(just): add a build module, retire the recipes nothing calls 2026-09-15 11:50:11 -07:00
supabase.just fix(ci): SMTP_PORT keeps its .env override and gains a floor 2026-08-26 19:10:14 -07:00
test.just fix(auth): the middleware stops deleting the session the route just set 2026-09-15 13:11:14 -07:00
trip.just feat(trips): export and import a trip as a single archive 2026-09-04 11:09:11 -07:00

trippin

build

Personal travel logging app — trips, points of interest, photos, and a map-first browsing surface. A migration of the old FotoLog.

Fresh 2 (Deno) + Preact islands · Supabase (Postgres, Auth, Storage) · Lit for the Mapbox layer · Tailwind CSS 4 · an in-process background worker.


Quick Start

just setup                       # check tools, install git hooks, cache deps, create .env
supabase start                   # local Supabase stack (Docker)
just test::env                   # generate .env.test = local Supabase creds + your .env
just dev                         # http://localhost:5173

just setup verifies you have deno, docker, just, and supabase on PATH and tells you where to get whatever is missing.

Migrations run automatically at app boot (src/main.ts), so the first just dev brings the schema up. To apply them without starting the app: just migrate up.

Grant yourself admin: just admin::grant you@example.com.

Re-run just test::env after every supabase start (keys rotate) and after every edit to .env. just dev reads .env.test, not .env.


Environment

File Role
env.example template — every supported variable, documented
.env the file you edit; dev-safe values only. Copied wholesale into .env.test
.env.test generated by just test::env — local Supabase credentials + everything from .env. Never hand-edit
.env.prod manually maintained, points at live infra. Nothing reads it unless you ask: just migrate up .env.prod, ENV_FILE=.env.prod just psql

The app runs without the optional keys — Mapbox, OpenRouter (vision + embeddings), Postmark/SMTP, and Turnstile each degrade rather than crash. See env.example for what each one switches on.


Common Commands

Everything goes through just. Run just with no arguments for the full list including submodules.

Command What it does
just dev Vite dev server (ensures the Supabase stack is up first)
just build::prod production build — Vite + the Lit component bundle
just build::components rebuild src/static/components-map.js only (after editing src/lib/web-components/)
just build::icons regenerate the inline icon registry from src/assets/icons/
just check fast gate: format, lint, type-check
just validate what CI runs: check + coverage ratchet + audit + secrets + SAST + fuzz
just test::all full test suite
just test::units unit/integration only (excludes src/e2e/ and src/fuzz/)
just test::e2e end-to-end only
just test::coverage tests with coverage; refreshes .coverage/summary.json
just psql psql shell against the same DB just dev uses
just migrate up [env] apply pending SQL migrations (defaults to .env.test)
just supabase::status local stack URLs, keys, running services
just supabase::verify .env.prod preflight a Supabase project against what the app needs
just admin::grant <email> grant the site-admin role
just release <version> bump version, commit, tag, push

Testing

Tests run against real Supabase — no mocking. Unit and integration tests live next to their source (foo.ts → foo.test.ts); end-to-end tests live in src/e2e/.

supabase start && just test::env   # once per stack restart
just test::all

Two isolation helpers, both from src/lib/testing/mod.ts:

  • createTestUser() — a real, pre-confirmed Supabase Auth user plus a cleanup() you must always call in a finally.
  • withTransaction() — wraps the body in a transaction that is always rolled back. Only valid when the code under test uses the single PoolClient you hand it; code that opens its own pool connections (e.g. processEmailBatch) will not see the uncommitted rows.

The dev stack and the test suite share one database. Three rules keep a test run from spilling into your just dev session: fixtures only ever address @test.invalid / @example.com (the worker refuses to dial reserved domains), just test::_tested purges accounts created during the run, and the middleware treats an auth 5xx as a transient outage rather than a logout. Local GoTrue mail lands in Inbucket at http://localhost:33304.

Full conventions, including how to test Fresh 2 route handlers and middleware: AGENTS.md.


Layout

src/
  main.ts              # entry — env validation, migrations, worker start, Fresh app
  routes/              # file-system routes (pages + /api)
  islands/             # interactive Preact islands
  components/          # server-rendered Preact components
  lib/
    supabase.ts        # server-side Supabase client (SSR cookie auth)
    db/                # Postgres pool + migration runner
    worker/            # queue, registry, task/email batches, loop
    curation/          # photo curation lifecycle — the largest subsystem
    pois/ trips/ teams/ sharing/ auth/ admin/ email/ globe/
    web-components/    # Lit — Mapbox-coupled elements only
    testing/           # test factories and helpers
  migrations/          # numbered SQL, applied at boot
  e2e/                 # end-to-end tests
  static/              # served as-is, incl. the built components-map.js

Root holds config only (deno.json, justfile + *.just modules, Dockerfile).


Architecture Notes

Four constraints that are easy to violate and expensive to unwind:

  • Queue writes go through src/lib/worker/queue.ts. Never INSERT into tasks or emails directly — the queue is where dedup keys are derived, and that's what makes enqueues safe to retry.
  • Lit is for Mapbox only. trip-map, globe-view, map controls — and nothing else; just test::rules fails on a custom element defined anywhere else, or rendered inside an island's JSX. Every other piece of UI is Preact so it gets SSR, typed props, and one build path. Cross-framework signalling uses document-level CustomEvents.
  • Map UI lives inside the map. Don't absolutely-position controls over a map component from outside; implement MapControl and register it.
  • Storage URLs are signed. Trip media is private and served through short-lived signed URLs generated server-side. Avatars are the one exception — their own public-read bucket.

Where the Docs Live

Doc What it holds
AGENTS.md the operational "how" — dev commands, testing conventions, key constraints. Symlinked as CLAUDE.md
src/AGENTS.md frontend rules — component policy, Button pattern, theme tokens, form/drawer layout, browser-debugging order
wiki/README.md the "why" — design stances, architecture rationale, and debugging post-mortems. Start here for background on a decision
wiki/GLOSSARY.md canonical domain vocabulary. Use these exact terms in code, prompts, and docs
LEARN.md the five load-bearing files, in reading order, for getting oriented fast
DEPLOY.md production: Supabase Cloud provisioning, prod .env, image build, VPS run, verification, rollback
openspec/ spec-driven change proposals and living capability specs — managed by the /opsx:* tooling, not hand-edited

Deployment

Single Docker image, one container, behind Caddy on a VPS; Supabase Cloud for Postgres, Auth, and Storage. The background worker runs in the same process as the web app, which is why exactly one container may run at a time.

just build::docker      # docker build -t trippin .

There is no compose recipe: the docker-compose.yml the host runs is not committed — DEPLOY.md carries it and the Caddy config to paste onto the server.

Full production procedure — provisioning, TLS-to-Postgres gotchas, the prod environment file, first-boot verification, and rollback — is in DEPLOY.md.

CI is Forgejo Actions (.forgejo/workflows/build.yaml); it runs just validate against a warm Supabase stack on the runner, serialized repo-wide.