Files
tatlock-ui/CLAUDE.md
T
jpmschweitzerandClaude c26b54398c docs: replace AGENTS.md with a repo-specific CLAUDE.md
One agent doc per repo, and it is CLAUDE.md. Written fresh rather than
reformatted. PHILOSOPHY.md linked to the old file, so that pointer moves
with it, and its standing requirement to be read before working here is
carried forward rather than lost in the rewrite.

Two claims did not survive verification. The app is published on 9999,
not the tower:8092 the old file gave, and it pointed at portainer-core
for full-stack documentation -- that repo is deprecated and must not be
used as a source of infra facts.

Establishing what is live needs a different method here: there is no
sys.modules to read, since the container holds a compiled web build
rather than source. A transitive walk of import/export/part directives
from lib/main.dart found 8 of 132 files unreachable, and five of those
are exactly what runs in production. They are conditional-import targets
-- `import 'a.dart' if (dart.library.html) 'b.dart'` -- and a walk that
takes the first string misses the branch. Since this ships as Flutter
web, the _web half is live and the _stub/_native half is dormant. The
naive reading was not merely wrong but inverted.

Of the three genuinely unreferenced files, stack_model.dart is imported
only by its own test, so the suite is green and vouches for a model the
app never uses. permission_gate.dart sits next to an unimplemented auth
redesign and is recorded as undetermined rather than dead.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-09 03:15:54 +02:00

7.9 KiB

CLAUDE.md — tatlock-ui

Flutter/Dart frontend for the homelab — the dashboard at home.schweitz.net. Riverpod state, Material 3, Go-Router. Built as a Flutter web app, served as compiled static assets by nginx in the tatlock-ui container. pubspec.yaml version 1.7.1+1, package name tatlock_ui, Dart SDK ^3.10.4.

It is a client, not a service. It talks to core-api (:8083) and tatlock (:8000); it exposes no API of its own and has no /openapi.json.

Read first

  • PHILOSOPHY.md — the system vision and the architectural patterns all work should move toward. The previous AGENTS.md made this a mandatory pre-work read and that requirement is carried forward deliberately.
  • docs/UI_LAYOUT.md — read before touching the widget tree. Check lib/shared/components/ for an existing component before building a new one, and look at how a comparable feature already does it. Deviating from the established patterns is the main source of drift here.
  • docs/ also holds ARCHITECTURE.md, API_INTEGRATION.md, TESTING.md, THEMING.md, DATAGRID.md, DEPLOYMENT.md.

Ports and where it runs

Container tatlock-ui, 127.0.0.1:9999 -> 80 (nginx serving the web build)
External home.schweitz.net
Backends core-api :8083, tatlock :8000

The old AGENTS.md said the app is "available at http://tower:8092". That is stale — the published port is 9999, verified against docker ps on 2026-08-09.

It also pointed at portainer-core for full-stack documentation. portainer-core is deprecated and must not be used as a source of infra facts; it was merged into system-admin-toj/containers/. The live inventory is CONTAINERS.md there.

The Gitea SSO-bypass trick is real and still works: http://localhost:3002 reaches Gitea directly, verified returning {"version":"1.27.1"}. Useful for reading a sibling repo's raw files without going through Authentik.

Layout

lib/main.dart → lib/app.dart; lib/core/ (api, auth, config, error, providers, semantics, theme), lib/features/<room>/ (control_room, front_hall, media_room, parlor, security, settings), lib/routing/, lib/shared/ (components, layouts, theme, widgets). 132 Dart files under lib/, 24 test files.

Establishing what is live — and the trap in it

There is no sys.modules here and nothing to docker exec into: the container holds compiled assets, not source. The Dart analogue is a transitive walk of import/export/part directives from lib/main.dart, resolving package:tatlock_ui/… to lib/…. Run 2026-08-09: 124 of 132 files reachable, 8 not.

Do not read that as a delete list. Five of the eight are the code that actually runs in production. They are conditional-import targets:

import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart';

A naive walk captures the first string and misses the branch. Since this app ships as Flutter web, the _web.dart half is the live one and the _stub/_native half is the dormant one — the exact inverse of what the reachability count suggests. The five: api_client_web.dart, web_utils_web.dart, url_strategy_web.dart, url_state_web.dart, iframe_view_web.dart. Find them all with grep -rn "if (dart.library" lib/.

That leaves three genuinely unreferenced files, and they are not all the same thing:

File Status
lib/core/auth/permission_gate.dart no reference anywhere in lib/ or test/
lib/core/semantics/semantic_widget.dart no reference anywhere in lib/ or test/
lib/features/control_room/stacks/data/models/stack_model.dart referenced only by its own test

The third is the interesting one: stack_model_test.dart imports and exercises it, so the suite is green and gives confidence about a model the app never uses. A passing test is not evidence a thing is wired in.

Before deleting any of the three, check whether it is intended groundwork rather than debris — TODO_AUTH_REFACTOR.md describes an unimplemented auth redesign, and permission_gate.dart sits squarely in that area. Neither that file nor PLAN.md mentions it by name, so its status is undetermined, not dead. Ask before removing.

Tooling

flutter and dart resolve from /snap/bin, which is on the non-interactive PATH — so bare commands work here (unlike pql, which needs its absolute path).

flutter pub get
flutter test                 # 24 test files
flutter analyze              # static analysis; analysis_options.yaml at the repo root
flutter build web --release

Always add tests for new code before committing — happy path plus key edge cases, widget tests for widgets, unit tests for logic. Coverage should not decrease. Carried over from the previous AGENTS.md, which stated it as "no exceptions".

Note there is no CI test gate: .gitea/workflows/build.yml triggers only on v* tag push and goes straight to build and release. flutter test runs locally or not at all.

Work tracking

Work lives in pql, not a markdown TODO. This repo's vault is standalone — its tickets and its internal decisions live here in .pql/ and governance/, and travel with a clone, because .pql/changelog/ is committed and replayed by the git hooks (D-15). The databases are gitignored and rebuildable with pql plan rebuild.

pql is not on the non-interactive PATH — invoke it as /home/jpmschweitzer/.local/bin/pql. From inside this repo no --vault is needed: pql anchors at the nearest .git/ ancestor, which is this repo.

/home/jpmschweitzer/.local/bin/pql ticket list        # this repo's open work
/home/jpmschweitzer/.local/bin/pql plan whatsnext     # next unblocked item, with context
/home/jpmschweitzer/.local/bin/pql decisions list     # this repo's own decisions

Stack-level decisions that constrain this app live in the workspace vault and need the flag:

/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions list --domain tatlock-ui

Note ticket new --decision D-N resolves ids within one vault, so a ticket here cannot link to a workspace decision. Cite the id in the ticket body instead.

PLAN.md and TODO_AUTH_REFACTOR.md predate this convention. Treat them as research notes; new work goes in pql.

Git

  • History is linear — no merge commits. Work on main, or a short-lived branch that is fast-forwarded and deleted.
  • Conventional Commits: feat:, fix:, refactor:, docs:, chore:.
  • Stage explicitly. Never git add -A — denied by policy, and it sweeps in whatever else is dirty.
  • Every version increment gets a tag — vX.Y.Z, on the major.minor.patch part, not the build number.
  • Update CHANGELOG.md for every user-facing change.

.claude/settings.local.json is currently tracked in git here. Contents are benign — a four-entry permission allow list, no env block, no secrets — but it is machine-local state that should not be shared. There is now a .gitignore rule for it, and that rule is inert: git applies ignore rules only to untracked paths, so edits still show in git status and still get committed. It starts working only after git rm --cached .claude/settings.local.json, which is a history decision and was deliberately left out of normalization.

Consequence for checking: plain git check-ignore prints nothing for this path — it consults the index — which looks identical to "no rule exists". Use --no-index to test the rule itself.

Releasing

  1. Bump version in pubspec.yaml (the major.minor.patch part).
  2. Move [Unreleased] entries into a dated section in CHANGELOG.md.
  3. Stage by name, commit, tag vX.Y.Z, git push origin main --tags.
  4. Gitea CI builds the web app in Docker, pushes :latest and :vX.Y.Z; Watchtower deploys.
  5. Verify at home.schweitz.net, or curl -I http://localhost:9999.

Rollback: in Portainer, point the image tag at the previous version.