Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52048c03ce | ||
|
|
f04672f350 | ||
|
|
809f900bd5 | ||
|
|
69de933ab0 | ||
|
|
3776a4012b | ||
|
|
05948b41a6 | ||
|
|
9385dd253a | ||
|
|
e816b0cb35 | ||
|
|
36fdffe644 | ||
|
|
110116f586 | ||
|
|
abc8520a0e | ||
|
|
024a0ce074 | ||
|
|
5181b12d7a | ||
|
|
2baf8b4ba3 | ||
|
|
c26b54398c | ||
|
|
f9f10b322f | ||
|
|
13991c7afe |
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"PQL_VAULT": "/mnt/media/Projects/tatlock-ui"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(pql)",
|
||||||
|
"Bash(pql *)",
|
||||||
|
"Bash(/home/jpmschweitzer/.local/bin/pql:*)",
|
||||||
|
"Bash(git status:*)",
|
||||||
|
"Bash(git log:*)",
|
||||||
|
"Bash(git diff:*)",
|
||||||
|
"Bash(git branch:*)",
|
||||||
|
"Bash(flutter test:*)",
|
||||||
|
"Bash(flutter analyze:*)",
|
||||||
|
"Bash(flutter pub get:*)",
|
||||||
|
"Bash(flutter pub outdated:*)",
|
||||||
|
"Bash(flutter build web:*)",
|
||||||
|
"Bash(dart analyze:*)",
|
||||||
|
"Bash(dart format:*)",
|
||||||
|
"Bash(docker logs tatlock-ui:*)",
|
||||||
|
"Bash(curl -sI http://localhost:9999/*)",
|
||||||
|
"mcp__chrome-devtools__take_screenshot",
|
||||||
|
"mcp__puppeteer__puppeteer_evaluate",
|
||||||
|
"mcp__chrome-devtools__navigate_page",
|
||||||
|
"mcp__chrome-devtools__take_snapshot"
|
||||||
|
],
|
||||||
|
"deny": [
|
||||||
|
"Bash(/mnt/media/Projects/cladmin/ops/bin/toj)",
|
||||||
|
"Bash(/mnt/media/Projects/cladmin/ops/bin/toj:*)",
|
||||||
|
"Bash(chmod -R 777 *)",
|
||||||
|
"Bash(chmod 777 *)",
|
||||||
|
"Bash(dart pub publish*)",
|
||||||
|
"Bash(dd if=*)",
|
||||||
|
"Bash(find * -delete*)",
|
||||||
|
"Bash(find * -exec*)",
|
||||||
|
"Bash(flutter pub publish*)",
|
||||||
|
"Bash(git * add --all*)",
|
||||||
|
"Bash(git * add -A*)",
|
||||||
|
"Bash(git * add .)",
|
||||||
|
"Bash(git * branch -D *)",
|
||||||
|
"Bash(git * checkout -- *)",
|
||||||
|
"Bash(git * clean -fd*)",
|
||||||
|
"Bash(git * clean -fdx*)",
|
||||||
|
"Bash(git * commit --no-verify*)",
|
||||||
|
"Bash(git * merge --no-ff*)",
|
||||||
|
"Bash(git * push --force*)",
|
||||||
|
"Bash(git * push -f*)",
|
||||||
|
"Bash(git * reset --hard*)",
|
||||||
|
"Bash(git * restore .*)",
|
||||||
|
"Bash(git add --all*)",
|
||||||
|
"Bash(git add -A*)",
|
||||||
|
"Bash(git add .)",
|
||||||
|
"Bash(git branch -D *)",
|
||||||
|
"Bash(git checkout -- *)",
|
||||||
|
"Bash(git clean -fd*)",
|
||||||
|
"Bash(git clean -fdx*)",
|
||||||
|
"Bash(git commit --no-verify*)",
|
||||||
|
"Bash(git merge --no-ff*)",
|
||||||
|
"Bash(git push --force*)",
|
||||||
|
"Bash(git push -f*)",
|
||||||
|
"Bash(git reset --hard*)",
|
||||||
|
"Bash(git restore .*)",
|
||||||
|
"Bash(mkfs*)",
|
||||||
|
"Bash(rm -rf $HOME)",
|
||||||
|
"Bash(rm -rf /)",
|
||||||
|
"Bash(rm -rf ~)",
|
||||||
|
"Bash(su *)",
|
||||||
|
"Bash(sudo *)",
|
||||||
|
"Bash(toj)",
|
||||||
|
"Bash(toj:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"mcp__chrome-devtools__take_screenshot",
|
|
||||||
"mcp__puppeteer__puppeteer_evaluate",
|
|
||||||
"mcp__chrome-devtools__navigate_page",
|
|
||||||
"mcp__chrome-devtools__take_snapshot"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.pql/changelog/*.sql merge=union
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Trigger only. The checks live in the Makefile, where they can be read, run by
|
||||||
|
# hand (`make pre-push`), and changed under review.
|
||||||
|
#
|
||||||
|
# This file is identical in every repo in this workspace, deliberately: the call
|
||||||
|
# surface is the same everywhere even though what each gate runs is not, so
|
||||||
|
# nobody has to read a repo to find out how to check it (D-27).
|
||||||
|
#
|
||||||
|
# Enable per clone with: git config core.hooksPath .githooks
|
||||||
|
# Never bypass with --no-verify. Suppress a specific finding deliberately
|
||||||
|
# instead, with a reason — see `make pre-push`.
|
||||||
|
set -euo pipefail
|
||||||
|
exec make -C "$(git rev-parse --show-toplevel)" pre-push
|
||||||
+24
@@ -121,3 +121,27 @@ secrets/
|
|||||||
uploads/
|
uploads/
|
||||||
logs/
|
logs/
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
|
||||||
|
# Claude Code user-specific settings.
|
||||||
|
#
|
||||||
|
# WARNING: this file is currently TRACKED, so this rule does NOTHING yet. Git
|
||||||
|
# applies ignore rules only to untracked paths; edits to a tracked file still
|
||||||
|
# show in status and still get committed. It takes effect only after
|
||||||
|
# git rm --cached .claude/settings.local.json
|
||||||
|
# which is a history decision, deliberately left out of the 2026-08-09
|
||||||
|
# normalization pass. Contents are benign - a 4-entry permission allow list, no
|
||||||
|
# env block, no secrets - so this is hygiene, not an incident.
|
||||||
|
#
|
||||||
|
# Also: plain `git check-ignore` prints nothing for this path because it consults
|
||||||
|
# the index. Use `git check-ignore --no-index` to confirm the rule itself matches.
|
||||||
|
.claude/settings.local.json
|
||||||
|
.pql/*
|
||||||
|
!.pql/changelog/
|
||||||
|
|
||||||
|
# pql shims planted by `pql init` into the dir core.hooksPath points at.
|
||||||
|
# Per-clone: each embeds the absolute path of the pql binary that planted it.
|
||||||
|
# Only .githooks/pre-push is shared.
|
||||||
|
.githooks/pre-commit
|
||||||
|
.githooks/post-merge
|
||||||
|
.githooks/post-checkout
|
||||||
|
.githooks/post-rewrite
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- Changelog format marker, written by pql. Comments only: this file
|
||||||
|
-- is never executed — Import descends into the per-table directories
|
||||||
|
-- and does not read the changelog root.
|
||||||
|
--
|
||||||
|
-- A changelog carrying no marker is format 1, the shape that existed
|
||||||
|
-- before formats were versioned. An older format is migrated forward
|
||||||
|
-- by `pql plan upgrade` (and automatically from the post-merge hook);
|
||||||
|
-- a newer one is refused rather than replayed under rules this binary
|
||||||
|
-- does not know. See D-28 and docs/versions.md.
|
||||||
|
-- pql:changelog_format: 2.0.0
|
||||||
|
-- pql:written_by: 2.2.0
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 2.2.0
|
||||||
|
-- pql:canonical_version: 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Identity split (D-26): a ticket's stable, collision-proof identity is its
|
||||||
|
-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly
|
||||||
|
-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural
|
||||||
|
-- reference (parent, deps, history, labels) targets record_id, so a label
|
||||||
|
-- clash never corrupts the graph — only ticket_idmap needs a relabel.
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
record_id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_record_id TEXT REFERENCES tickets(record_id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
-- No CHECK enumeration: the ticket status vocabulary is per-vault
|
||||||
|
-- configurable (ticket_statuses in .pql/config.yaml). Validation lives
|
||||||
|
-- in Go (planning.StatusSet), so adding/renaming statuses needs no
|
||||||
|
-- schema change. The DEFAULT is a harmless fallback — CreateTicket
|
||||||
|
-- always inserts the configured default explicitly.
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog',
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ticket_idmap maps a record_id to its current friendly label (T-NNN).
|
||||||
|
-- ticket_id is intentionally NOT globally unique: two uncoordinated clones
|
||||||
|
-- can mint the same label, which surfaces as a duplicate-label collision
|
||||||
|
-- (detected at replay) and is fixed with "pql ticket relabel".
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_idmap (
|
||||||
|
record_id TEXT PRIMARY KEY REFERENCES tickets(record_id),
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_record_id, blocked_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_record_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 2.2.0
|
||||||
|
-- pql:canonical_version: 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Identity split (D-26): a ticket's stable, collision-proof identity is its
|
||||||
|
-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly
|
||||||
|
-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural
|
||||||
|
-- reference (parent, deps, history, labels) targets record_id, so a label
|
||||||
|
-- clash never corrupts the graph — only ticket_idmap needs a relabel.
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
record_id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_record_id TEXT REFERENCES tickets(record_id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
-- No CHECK enumeration: the ticket status vocabulary is per-vault
|
||||||
|
-- configurable (ticket_statuses in .pql/config.yaml). Validation lives
|
||||||
|
-- in Go (planning.StatusSet), so adding/renaming statuses needs no
|
||||||
|
-- schema change. The DEFAULT is a harmless fallback — CreateTicket
|
||||||
|
-- always inserts the configured default explicitly.
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog',
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ticket_idmap maps a record_id to its current friendly label (T-NNN).
|
||||||
|
-- ticket_id is intentionally NOT globally unique: two uncoordinated clones
|
||||||
|
-- can mint the same label, which surfaces as a duplicate-label collision
|
||||||
|
-- (detected at replay) and is fixed with "pql ticket relabel".
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_idmap (
|
||||||
|
record_id TEXT PRIMARY KEY REFERENCES tickets(record_id),
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_record_id, blocked_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_record_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'description', NULL, '`.claude/settings.local.json` is committed to this repo. It is the per-clone local overrides file — the documented home for personal settings and for credentials — and it does not belong in version control at all.
|
||||||
|
|
||||||
|
STATE, verified 2026-08-11 rather than assumed:
|
||||||
|
- the file is tracked, with 2 commits touching it (both 2025-12-31)
|
||||||
|
- `.gitignore` already lists it at line 137, and line 130 carries the exact `git rm --cached` command in a comment
|
||||||
|
- gitignore does not apply to files git already tracks, which is why the rule has been inert since it was added
|
||||||
|
- this is the ONLY repo in the workspace where the file is tracked; desklock, library-desk and tatlock all ignore it correctly
|
||||||
|
|
||||||
|
NO CREDENTIALS WERE EVER COMMITTED. Both committed versions contain a `permissions` section and nothing else — zero env keys, checked at each commit rather than only at HEAD. This is a loaded trap, not a leak: the next person who adds an env key to this file commits a secret, and nothing will stop them because the ignore rule cannot fire on a tracked path.
|
||||||
|
|
||||||
|
Worth recording because it has already cost time once: a survey agent previously reported credentials committed across nine repos. Traced to source, it was this one file holding a four-entry permission list and no secrets. The finding was wrong and the file is still tracked, so the same false alarm is available to the next person who greps for it.
|
||||||
|
|
||||||
|
THE FOUR RULES IN IT ARE THE ONLY REASON TO PAUSE. They allow browser-automation MCP tools:
|
||||||
|
mcp__chrome-devtools__take_screenshot, take_snapshot, navigate_page
|
||||||
|
mcp__puppeteer__puppeteer_evaluate
|
||||||
|
|
||||||
|
Untracking removes them from anyone who clones. Two ways to go, and this wants deciding rather than defaulting:
|
||||||
|
- they are genuinely personal (one person''s browser tooling) — untrack and let each clone re-grant
|
||||||
|
- they are useful to anyone working on this UI — move them into `.claude/settings.json`, which is committed by design and already carries 16 allow and 43 deny rules
|
||||||
|
|
||||||
|
The second reading looks likelier for a front-end repo where visual verification is routine, but it is a judgement about how people work here, not something the file can answer.
|
||||||
|
|
||||||
|
FIX: `git rm --cached .claude/settings.local.json` and commit. The file stays on disk, so nobody loses their local settings; it simply stops being shared. Decide the four rules first, or they vanish quietly.', NULL, '2026-08-11 19:21:23', '2026-08-11 19:21:23.594', '2026-08-11 19:21:23.594', NULL, '6ae54cdfcaa87232e3074a23b88d1b82', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'status', 'backlog', 'done', NULL, '2026-08-11 19:27:15', '2026-08-11 19:27:15.269', '2026-08-11 19:27:15.269', NULL, 'e7784de0ed56f0679d0823a163b008bc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 2.2.0
|
||||||
|
-- pql:canonical_version: 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Identity split (D-26): a ticket's stable, collision-proof identity is its
|
||||||
|
-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly
|
||||||
|
-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural
|
||||||
|
-- reference (parent, deps, history, labels) targets record_id, so a label
|
||||||
|
-- clash never corrupts the graph — only ticket_idmap needs a relabel.
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
record_id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_record_id TEXT REFERENCES tickets(record_id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
-- No CHECK enumeration: the ticket status vocabulary is per-vault
|
||||||
|
-- configurable (ticket_statuses in .pql/config.yaml). Validation lives
|
||||||
|
-- in Go (planning.StatusSet), so adding/renaming statuses needs no
|
||||||
|
-- schema change. The DEFAULT is a harmless fallback — CreateTicket
|
||||||
|
-- always inserts the configured default explicitly.
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog',
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ticket_idmap maps a record_id to its current friendly label (T-NNN).
|
||||||
|
-- ticket_id is intentionally NOT globally unique: two uncoordinated clones
|
||||||
|
-- can mint the same label, which surfaces as a duplicate-label collision
|
||||||
|
-- (detected at replay) and is fixed with "pql ticket relabel".
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_idmap (
|
||||||
|
record_id TEXT PRIMARY KEY REFERENCES tickets(record_id),
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_record_id, blocked_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_record_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'T-1', '2026-08-11 19:21:23.466', '2026-08-11 19:21:23.466', NULL, '953ce74627a5dd3cc3f2c4e56e6fc6aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 2.2.0
|
||||||
|
-- pql:canonical_version: 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Identity split (D-26): a ticket's stable, collision-proof identity is its
|
||||||
|
-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly
|
||||||
|
-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural
|
||||||
|
-- reference (parent, deps, history, labels) targets record_id, so a label
|
||||||
|
-- clash never corrupts the graph — only ticket_idmap needs a relabel.
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
record_id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_record_id TEXT REFERENCES tickets(record_id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
-- No CHECK enumeration: the ticket status vocabulary is per-vault
|
||||||
|
-- configurable (ticket_statuses in .pql/config.yaml). Validation lives
|
||||||
|
-- in Go (planning.StatusSet), so adding/renaming statuses needs no
|
||||||
|
-- schema change. The DEFAULT is a harmless fallback — CreateTicket
|
||||||
|
-- always inserts the configured default explicitly.
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog',
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ticket_idmap maps a record_id to its current friendly label (T-NNN).
|
||||||
|
-- ticket_id is intentionally NOT globally unique: two uncoordinated clones
|
||||||
|
-- can mint the same label, which surfaces as a duplicate-label collision
|
||||||
|
-- (detected at replay) and is fixed with "pql ticket relabel".
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_idmap (
|
||||||
|
record_id TEXT PRIMARY KEY REFERENCES tickets(record_id),
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_record_id, blocked_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_record_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
-- Auto-generated by pql init. CREATE TABLE statements
|
||||||
|
-- for the planning schema; per-table dir keeps the changelog
|
||||||
|
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||||
|
-- idempotent so running schema files from each directory in
|
||||||
|
-- replay order is harmless.
|
||||||
|
--
|
||||||
|
-- Importer parses the markers below to detect schema drift
|
||||||
|
-- between the producing pql version and the local one — a
|
||||||
|
-- bumped canonical_version means projection rules changed
|
||||||
|
-- and replay must refuse rather than silently corrupt state.
|
||||||
|
-- pql:created_by: 2.2.0
|
||||||
|
-- pql:canonical_version: 2
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decisions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
|
CHECK(status IN ('active','superseded','resolved','open')),
|
||||||
|
date TEXT,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||||
|
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||||
|
ref_type TEXT NOT NULL
|
||||||
|
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||||
|
note TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (source_id, target_id, ref_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Identity split (D-26): a ticket's stable, collision-proof identity is its
|
||||||
|
-- record_id (a locally-generated ULID, planning.NewRecordID); the friendly
|
||||||
|
-- T-NNN label lives in ticket_idmap and may be reconciled. Every structural
|
||||||
|
-- reference (parent, deps, history, labels) targets record_id, so a label
|
||||||
|
-- clash never corrupts the graph — only ticket_idmap needs a relabel.
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
record_id TEXT PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||||
|
parent_record_id TEXT REFERENCES tickets(record_id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
-- No CHECK enumeration: the ticket status vocabulary is per-vault
|
||||||
|
-- configurable (ticket_statuses in .pql/config.yaml). Validation lives
|
||||||
|
-- in Go (planning.StatusSet), so adding/renaming statuses needs no
|
||||||
|
-- schema change. The DEFAULT is a harmless fallback — CreateTicket
|
||||||
|
-- always inserts the configured default explicitly.
|
||||||
|
status TEXT NOT NULL DEFAULT 'backlog',
|
||||||
|
priority TEXT DEFAULT 'medium'
|
||||||
|
CHECK(priority IN ('critical','high','medium','low')),
|
||||||
|
assigned_to TEXT,
|
||||||
|
team TEXT,
|
||||||
|
decision_ref TEXT REFERENCES decisions(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ticket_idmap maps a record_id to its current friendly label (T-NNN).
|
||||||
|
-- ticket_id is intentionally NOT globally unique: two uncoordinated clones
|
||||||
|
-- can mint the same label, which surfaces as a duplicate-label collision
|
||||||
|
-- (detected at replay) and is fixed with "pql ticket relabel".
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_idmap (
|
||||||
|
record_id TEXT PRIMARY KEY REFERENCES tickets(record_id),
|
||||||
|
ticket_id TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||||
|
blocker_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
blocked_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (blocker_record_id, blocked_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
field TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_by TEXT,
|
||||||
|
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT UNIQUE,
|
||||||
|
canonical_version INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||||
|
ticket_record_id TEXT NOT NULL REFERENCES tickets(record_id),
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
deleted_at TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
canonical_version INTEGER,
|
||||||
|
PRIMARY KEY (ticket_record_id, label)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ticket_idmap_label ON ticket_idmap(ticket_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'bug', NULL, '.claude/settings.local.json is tracked in git and should not be', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-08-11 19:21:23.458', '2026-08-11 19:21:23.458', NULL, 'b68f6240b59c932e1b81744ce2e9f054', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'bug', NULL, '.claude/settings.local.json is tracked in git and should not be', '`.claude/settings.local.json` is committed to this repo. It is the per-clone local overrides file — the documented home for personal settings and for credentials — and it does not belong in version control at all.
|
||||||
|
|
||||||
|
STATE, verified 2026-08-11 rather than assumed:
|
||||||
|
- the file is tracked, with 2 commits touching it (both 2025-12-31)
|
||||||
|
- `.gitignore` already lists it at line 137, and line 130 carries the exact `git rm --cached` command in a comment
|
||||||
|
- gitignore does not apply to files git already tracks, which is why the rule has been inert since it was added
|
||||||
|
- this is the ONLY repo in the workspace where the file is tracked; desklock, library-desk and tatlock all ignore it correctly
|
||||||
|
|
||||||
|
NO CREDENTIALS WERE EVER COMMITTED. Both committed versions contain a `permissions` section and nothing else — zero env keys, checked at each commit rather than only at HEAD. This is a loaded trap, not a leak: the next person who adds an env key to this file commits a secret, and nothing will stop them because the ignore rule cannot fire on a tracked path.
|
||||||
|
|
||||||
|
Worth recording because it has already cost time once: a survey agent previously reported credentials committed across nine repos. Traced to source, it was this one file holding a four-entry permission list and no secrets. The finding was wrong and the file is still tracked, so the same false alarm is available to the next person who greps for it.
|
||||||
|
|
||||||
|
THE FOUR RULES IN IT ARE THE ONLY REASON TO PAUSE. They allow browser-automation MCP tools:
|
||||||
|
mcp__chrome-devtools__take_screenshot, take_snapshot, navigate_page
|
||||||
|
mcp__puppeteer__puppeteer_evaluate
|
||||||
|
|
||||||
|
Untracking removes them from anyone who clones. Two ways to go, and this wants deciding rather than defaulting:
|
||||||
|
- they are genuinely personal (one person''s browser tooling) — untrack and let each clone re-grant
|
||||||
|
- they are useful to anyone working on this UI — move them into `.claude/settings.json`, which is committed by design and already carries 16 allow and 43 deny rules
|
||||||
|
|
||||||
|
The second reading looks likelier for a front-end repo where visual verification is routine, but it is a judgement about how people work here, not something the file can answer.
|
||||||
|
|
||||||
|
FIX: `git rm --cached .claude/settings.local.json` and commit. The file stays on disk, so nobody loses their local settings; it simply stops being shared. Decide the four rules first, or they vanish quietly.', 'backlog', 'high', NULL, NULL, NULL, '2026-08-11 19:21:23.458', '2026-08-11 19:21:23.593', NULL, 'dad1d9ab6415c597def74a213e0d914e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FZ4HDS0AGMK8R3WS0RY7BMJW', 'bug', NULL, '.claude/settings.local.json is tracked in git and should not be', '`.claude/settings.local.json` is committed to this repo. It is the per-clone local overrides file — the documented home for personal settings and for credentials — and it does not belong in version control at all.
|
||||||
|
|
||||||
|
STATE, verified 2026-08-11 rather than assumed:
|
||||||
|
- the file is tracked, with 2 commits touching it (both 2025-12-31)
|
||||||
|
- `.gitignore` already lists it at line 137, and line 130 carries the exact `git rm --cached` command in a comment
|
||||||
|
- gitignore does not apply to files git already tracks, which is why the rule has been inert since it was added
|
||||||
|
- this is the ONLY repo in the workspace where the file is tracked; desklock, library-desk and tatlock all ignore it correctly
|
||||||
|
|
||||||
|
NO CREDENTIALS WERE EVER COMMITTED. Both committed versions contain a `permissions` section and nothing else — zero env keys, checked at each commit rather than only at HEAD. This is a loaded trap, not a leak: the next person who adds an env key to this file commits a secret, and nothing will stop them because the ignore rule cannot fire on a tracked path.
|
||||||
|
|
||||||
|
Worth recording because it has already cost time once: a survey agent previously reported credentials committed across nine repos. Traced to source, it was this one file holding a four-entry permission list and no secrets. The finding was wrong and the file is still tracked, so the same false alarm is available to the next person who greps for it.
|
||||||
|
|
||||||
|
THE FOUR RULES IN IT ARE THE ONLY REASON TO PAUSE. They allow browser-automation MCP tools:
|
||||||
|
mcp__chrome-devtools__take_screenshot, take_snapshot, navigate_page
|
||||||
|
mcp__puppeteer__puppeteer_evaluate
|
||||||
|
|
||||||
|
Untracking removes them from anyone who clones. Two ways to go, and this wants deciding rather than defaulting:
|
||||||
|
- they are genuinely personal (one person''s browser tooling) — untrack and let each clone re-grant
|
||||||
|
- they are useful to anyone working on this UI — move them into `.claude/settings.json`, which is committed by design and already carries 16 allow and 43 deny rules
|
||||||
|
|
||||||
|
The second reading looks likelier for a front-end repo where visual verification is routine, but it is a judgement about how people work here, not something the file can answer.
|
||||||
|
|
||||||
|
FIX: `git rm --cached .claude/settings.local.json` and commit. The file stays on disk, so nobody loses their local settings; it simply stops being shared. Decide the four rules first, or they vanish quietly.', 'done', 'high', NULL, NULL, NULL, '2026-08-11 19:21:23.458', '2026-08-11 19:27:15.269', NULL, 'be5da7217e2daee70cd57471ffd934fc', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# LLM Agent Instructions
|
|
||||||
|
|
||||||
This document contains instructions and documentation references for AI assistants working with this codebase.
|
|
||||||
|
|
||||||
> **📖 Important**: Before working on this project, read [PHILOSOPHY.md](PHILOSOPHY.md) to understand the system vision, architectural patterns, and design goals. All development should work towards realizing those patterns.
|
|
||||||
# AGENTS.md
|
|
||||||
|
|
||||||
> **Start every session by reading this file.**
|
|
||||||
> This file outlines the operational protocols, coding standards, and architectural decisions for this Flutter project.
|
|
||||||
|
|
||||||
## 1. Agent Operational Protocols
|
|
||||||
|
|
||||||
### 🧠 Work Patterns (Plan-Act-Reflect)
|
|
||||||
* **Plan:** Before writing code, briefly outline your plan. Identify which files you will touch and what the side effects might be.
|
|
||||||
* **Act:** Execute the changes in small, atomic steps.
|
|
||||||
* **Reflect:** After coding, verify your work. Did you break existing tests? Did you add new tests?
|
|
||||||
|
|
||||||
### 🌐 Internal Service Access
|
|
||||||
* **git.schweitz.net**: Access via `http://localhost:3002` (direct Gitea) to bypass Authentik SSO
|
|
||||||
* Example: `curl http://localhost:3002/jpmschweitzer/library-desk/raw/branch/main/README.md`
|
|
||||||
* Public repos are readable without authentication
|
|
||||||
* Related repos: , `core-api`, `tatlock`, `library-desk`, `scheduler`, `portainer-core`
|
|
||||||
|
|
||||||
### 🐳 Deployment & Infrastructure
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT: Service Port Reference**
|
|
||||||
| Service | LAN Port | External URL | Notes |
|
|
||||||
|---------|----------|--------------|-------|
|
|
||||||
| **Core API** | 8083 | `api.schweitz.net` | FastAPI backend for this UI |
|
|
||||||
| **Tatlock API** | 8000 | `tatlock.schweitz.net` | Legacy Python API (Ollama proxy) |
|
|
||||||
| **Tatlock UI** | 9999 | `home.schweitz.net` | This Flutter app |
|
|
||||||
|
|
||||||
* **Full stack documentation**: Available in the `portainer-core` repo
|
|
||||||
* Access: `curl http://192.168.86.149:3002/jpmschweitzer/portainer-core/raw/branch/main/CONTAINERS.md`
|
|
||||||
* Contains: All service ports, URLs, Redis DB allocations, external domains
|
|
||||||
* **Health checks**:
|
|
||||||
* Core API: `curl http://192.168.86.149:8083/health`
|
|
||||||
* Tatlock API: `curl http://192.168.86.149:8000/health`
|
|
||||||
|
|
||||||
### 🛡️ Git Discipline
|
|
||||||
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
|
|
||||||
* `feat: add user login endpoint`
|
|
||||||
* `fix: resolve database connection timeout`
|
|
||||||
* `refactor: split monolith dependency file`
|
|
||||||
* **Atomic Commits:** Keep commits small. One logical change = one commit.
|
|
||||||
* **Version Tagging:** Every version increment (major.minor.patch, not build count) must have a corresponding git tag.
|
|
||||||
* Format: `v{major}.{minor}.{patch}` (e.g., `v0.3.0`)
|
|
||||||
* Tag after updating `pubspec.yaml` version and CHANGELOG
|
|
||||||
* Push tags with `git push --tags`
|
|
||||||
|
|
||||||
### 🚀 Release Procedure
|
|
||||||
|
|
||||||
This project uses version-tag-based CI/CD. Releases trigger automated Docker builds and deployments.
|
|
||||||
|
|
||||||
**Release Steps:**
|
|
||||||
|
|
||||||
1. Update version in `pubspec.yaml` (bump major.minor.patch, not build number)
|
|
||||||
2. Update `CHANGELOG.md` with changes under `## [x.x.x] - YYYY-MM-DD`
|
|
||||||
3. Commit changes: `git commit -m "chore: release vX.X.X"`
|
|
||||||
4. Create git tag: `git tag vX.X.X`
|
|
||||||
5. Push with tags: `git push origin main --tags`
|
|
||||||
|
|
||||||
CI/CD auto-triggers when a tag starting with `v` is pushed.
|
|
||||||
|
|
||||||
**What happens on release:**
|
|
||||||
|
|
||||||
* Gitea CI builds Flutter web app in Docker
|
|
||||||
* Image pushed to `git.schweitz.internal/jpmschweitzer/tatlock-ui:latest` and `:vX.X.X`
|
|
||||||
* Watchtower detects new image and auto-updates running container
|
|
||||||
* App available at `http://tower:8092` (and eventually `home.schweitz.net`)
|
|
||||||
|
|
||||||
**Rollback:**
|
|
||||||
|
|
||||||
* In Portainer, update image tag to previous version (e.g., `:v0.2.0`)
|
|
||||||
* Or: `docker pull git.schweitz.internal/jpmschweitzer/tatlock-ui:v0.2.0`
|
|
||||||
|
|
||||||
### 🧪 Testing Requirements
|
|
||||||
|
|
||||||
* **Always add tests for new code before committing.** No exceptions.
|
|
||||||
* Tests should cover the happy path and key edge cases.
|
|
||||||
* Run `flutter test` before committing to ensure all tests pass.
|
|
||||||
* For widgets: use widget tests. For business logic: use unit tests.
|
|
||||||
* Code coverage should not decrease with new commits.
|
|
||||||
|
|
||||||
### 📝 Changelog Maintenance
|
|
||||||
|
|
||||||
* **Update `CHANGELOG.md`** with every user-facing change.
|
|
||||||
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
|
|
||||||
|
|
||||||
### 🎨 UI Patterns (MUST READ BEFORE CHANGES)
|
|
||||||
|
|
||||||
* **Before modifying the widget tree**, read `docs/UI_LAYOUT.md` to understand established patterns.
|
|
||||||
* Investigate existing implementations in the codebase before creating new components.
|
|
||||||
* **DO NOT** reinvent wheels - check if shared components already exist in `lib/shared/components/`.
|
|
||||||
* Look at similar features for reference patterns (e.g., how other list views, forms, or CRUD screens are built).
|
|
||||||
* Deviating from established patterns creates inconsistency and technical debt.
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# 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](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](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:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
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).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 (workspace 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/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.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# tatlock-ui — the repo's command surface (D-27).
|
||||||
|
#
|
||||||
|
# Flutter rather than Python, so there is no venv and no PYTHON here. The
|
||||||
|
# reason the targets still exist under these names is the point of D-27: an
|
||||||
|
# agent or a person can run `make test` in any repo in this workspace without
|
||||||
|
# first working out which stack it is.
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show this help
|
||||||
|
@grep -hE '^[a-z][a-z0-9_-]*:.*?## ' $(MAKEFILE_LIST) \
|
||||||
|
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
.PHONY: setup
|
||||||
|
setup: ## Fetch dependencies and generate code (run this after a fresh clone)
|
||||||
|
flutter pub get
|
||||||
|
$(MAKE) generate
|
||||||
|
@ci/check_codegen.sh
|
||||||
|
|
||||||
|
.PHONY: generate
|
||||||
|
generate: ## Regenerate freezed/json_serializable/riverpod sources
|
||||||
|
dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
.PHONY: check-codegen
|
||||||
|
check-codegen: ## Prove every part directive has a generated file on disk
|
||||||
|
@ci/check_codegen.sh
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: ## Run the widget and unit tests
|
||||||
|
@ci/check_codegen.sh \
|
||||||
|
|| { echo "FAIL — generated sources missing or stale; run: make setup"; exit 69; }
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
# Why the guard above: *.freezed.dart and lib/**/*.g.dart are gitignored, so a
|
||||||
|
# fresh clone has none of them and most of the suite fails to compile rather
|
||||||
|
# than to assert. On 2026-08-09 that read as "26 passed, 17 failed" — which
|
||||||
|
# looks like broken tests and is actually a missing build step. After
|
||||||
|
# generating, the same suite is 452 passed. A test run that cannot compile
|
||||||
|
# should say so in those words.
|
||||||
|
#
|
||||||
|
# `setup` and `test` both call ci/check_codegen.sh rather than one calling
|
||||||
|
# the other's target, because `setup`'s job is "make the tree usable" (fails
|
||||||
|
# loud if codegen silently produced less than the tree needs) and `test`'s
|
||||||
|
# job is "is the tree usable right now" (fails loud if nobody ran setup at
|
||||||
|
# all, or ran it before a source file changed). Same check, two different
|
||||||
|
# questions, so a shared script rather than a shared Make target — a Make
|
||||||
|
# target can only be reused by depending on it, which would make `test`
|
||||||
|
# imply `flutter pub get` and `build_runner`, both slow, every run.
|
||||||
|
#
|
||||||
|
# The check walks every `part '<name>.g.dart'`/`part '<name>.freezed.dart'`
|
||||||
|
# directive under lib/ and confirms the named sibling file exists — not one
|
||||||
|
# sentinel file (the previous guard checked only
|
||||||
|
# user_preferences.freezed.dart, which would have missed 44 of the 45
|
||||||
|
# directives that exist today). See ci/check_codegen.sh for why this is
|
||||||
|
# preferred over `flutter analyze`: cheaper, and it targets exactly the
|
||||||
|
# generated/ungenerated distinction rather than static analysis in general.
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: ## Static analysis (analysis_options.yaml at the repo root)
|
||||||
|
flutter analyze
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Release build for the web target
|
||||||
|
flutter build web --release
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Remove build artefacts and the pub cache for this project
|
||||||
|
flutter clean
|
||||||
|
|
||||||
|
# git hands a hook a non-login shell, which never sees ~/.local/bin — where
|
||||||
|
# gitleaks lands. Without this the scan reports "not installed" on every push,
|
||||||
|
# which is a check that fails open (D-24).
|
||||||
|
export PATH := $(HOME)/.local/bin:/usr/local/bin:$(PATH)
|
||||||
|
|
||||||
|
.PHONY: secrets
|
||||||
|
secrets: ## Scan the commits about to be pushed for credentials
|
||||||
|
@ci/secrets.sh
|
||||||
|
|
||||||
|
# The call surface is identical in every repo; what it runs is not.
|
||||||
|
#
|
||||||
|
# `secrets` runs first, deliberately: it is the only failure here that cannot be
|
||||||
|
# undone by fixing it afterwards. A failed lint costs another commit; a pushed
|
||||||
|
# credential is cached and indexed whether or not it is later deleted.
|
||||||
|
#
|
||||||
|
# Some of these fail today, and are left wired anyway. The state was measured
|
||||||
|
# once and written down in T-56 rather than being worked around here — a gate
|
||||||
|
# quietly narrowed to what already passes is a gate that reports success for
|
||||||
|
# doing nothing, which is the failure this workspace keeps rediscovering.
|
||||||
|
.PHONY: pre-push
|
||||||
|
pre-push: secrets lint ## Everything the pre-push hook runs
|
||||||
|
@echo " -- not gated here yet: test (T-56)"
|
||||||
+1
-1
@@ -109,7 +109,7 @@ The butler (backend) runs the household. The UI opens the door.
|
|||||||
|
|
||||||
**Related Documents**:
|
**Related Documents**:
|
||||||
- **README.md**: Project setup and operational details
|
- **README.md**: Project setup and operational details
|
||||||
- **AGENTS.md**: LLM agent development guidelines
|
- **CLAUDE.md**: LLM agent development guidelines
|
||||||
- **PLAN.md**: Implementation roadmap and phases
|
- **PLAN.md**: Implementation roadmap and phases
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prove that generated sources exist for every part directive that requires
|
||||||
|
# one, after `make generate` has run. Lives here rather than inline in the
|
||||||
|
# Makefile so it can be read, run by hand (`make check-codegen`), and changed
|
||||||
|
# under review — same reasoning as ci/secrets.sh (D-27).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
# *.freezed.dart and lib/**/*.g.dart are gitignored, so a fresh or stale
|
||||||
|
# checkout can silently have some but not all of the files a `part`
|
||||||
|
# directive names. `dart run build_runner build` exits 0 whether it produced
|
||||||
|
# everything the tree needs or almost nothing — exit code is not evidence
|
||||||
|
# (T-47). What IS evidence: every `part '<name>';` directive in lib/ names a
|
||||||
|
# sibling file, and that file either exists or it doesn't. This walks every
|
||||||
|
# directive and checks its target directly, rather than trusting a single
|
||||||
|
# sentinel file (the old `test` guard checked one file,
|
||||||
|
# user_preferences.freezed.dart, and would have missed 44 other gaps).
|
||||||
|
#
|
||||||
|
# On 2026-08-09 this exact condition was 4 generated files present where 46
|
||||||
|
# were needed. `flutter analyze` would also catch it, but slower and later —
|
||||||
|
# this check is the cheapest thing that proves the same fact.
|
||||||
|
|
||||||
|
missing=0
|
||||||
|
checked=0
|
||||||
|
|
||||||
|
while IFS=: read -r file part_line; do
|
||||||
|
# grep -H prefixes exactly one "file:" — no line numbers, so a colon
|
||||||
|
# inside the match (there is none here, but be safe) can't split wrong.
|
||||||
|
# part_line looks like: part 'auth_state.freezed.dart';
|
||||||
|
target=$(printf '%s' "$part_line" | sed -E "s/^part '([^']+)';.*/\1/")
|
||||||
|
dir=$(dirname "$file")
|
||||||
|
checked=$((checked + 1))
|
||||||
|
if [ ! -f "$dir/$target" ]; then
|
||||||
|
echo "MISSING generated file: $dir/$target (required by 'part' directive in $file)" >&2
|
||||||
|
missing=$((missing + 1))
|
||||||
|
fi
|
||||||
|
done < <(grep -rH "^part '" lib --include='*.dart')
|
||||||
|
|
||||||
|
if [ "$checked" -eq 0 ]; then
|
||||||
|
echo "FAIL check-codegen — found zero 'part' directives under lib/; the check itself is broken, not the tree." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$missing" -gt 0 ]; then
|
||||||
|
echo "FAIL check-codegen — $missing of $checked generated files are missing. Run: make generate" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "check-codegen — $checked/$checked generated files present."
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Secret scan over the commits about to be pushed.
|
||||||
|
#
|
||||||
|
# Lives here rather than inside .githooks/pre-push so it can be read, run by
|
||||||
|
# hand (`make secrets`), and changed under review. A hook is a trigger; it is
|
||||||
|
# not a home for logic. Identical in every repo in this workspace (D-27).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
# A non-login shell — which is what git gives a hook — skips /etc/profile.d
|
||||||
|
# and never sees ~/.local/bin, where the gitleaks release tarball lands.
|
||||||
|
# Without this the scan reports "not installed" on every push.
|
||||||
|
[ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
if ! command -v gitleaks >/dev/null 2>&1; then
|
||||||
|
echo "FAIL secrets — gitleaks not installed, so this check would be a no-op pretending to pass." >&2
|
||||||
|
echo " https://github.com/gitleaks/gitleaks/releases → ~/.local/bin/gitleaks" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Scan the outgoing range, not full history. History here carries findings
|
||||||
|
# that are settled — test fixtures and vendored third-party code — and a gate
|
||||||
|
# that fails on something unfixable gets bypassed within a week. What matters
|
||||||
|
# is what is about to leave this machine.
|
||||||
|
if upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null); then
|
||||||
|
range="$upstream..HEAD"
|
||||||
|
elif git rev-parse --verify --quiet origin/main >/dev/null; then
|
||||||
|
range="origin/main..HEAD"
|
||||||
|
else
|
||||||
|
range=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$range" ]; then
|
||||||
|
gitleaks dir . --redact --no-banner --exit-code 1 || {
|
||||||
|
echo "FAIL secrets — gitleaks found a credential in the working tree." >&2; exit 1; }
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$(git log --oneline "$range" 2>/dev/null)" ] || exit 0
|
||||||
|
|
||||||
|
gitleaks git . --log-opts="$range" --redact --no-banner --exit-code 1 >/dev/null 2>&1 || {
|
||||||
|
echo "FAIL secrets — gitleaks found a credential in the commits being pushed." >&2
|
||||||
|
echo " inspect (values redacted): gitleaks git . --log-opts=\"$range\" --redact" >&2
|
||||||
|
echo " then remove and rotate it, or suppress deliberately:" >&2
|
||||||
|
echo " inline '# gitleaks:allow <reason>'" >&2
|
||||||
|
echo " or add the fingerprint to .gitleaksignore WITH a reason" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " ok secrets"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Decisions, Questions, Rejected
|
||||||
|
|
||||||
|
This directory holds structured planning records that pql parses
|
||||||
|
into pql.db. Each record is a `### [DQR]-N: Title` heading inside
|
||||||
|
a markdown file. Files live in three per-type subdirectories:
|
||||||
|
|
||||||
|
- `decisions/<domain>.md` — confirmed design decisions
|
||||||
|
- `questions/<domain>.md` — open questions that may resolve into
|
||||||
|
decisions or rejected proposals
|
||||||
|
- `rejected/<domain>.md` — rejected proposals (kept for the audit
|
||||||
|
trail)
|
||||||
|
|
||||||
|
The parser infers domain from the filename stem and record type
|
||||||
|
from the parent subdirectory.
|
||||||
|
|
||||||
|
D-records that propose implementation work link to `initiative`-type
|
||||||
|
tickets via `decision_ref`. Run `pql decisions show <id>
|
||||||
|
--with-tickets` to inspect implementation status.
|
||||||
|
|
||||||
|
## Recommended domains
|
||||||
|
|
||||||
|
Start with this canonical set; create files as records land in
|
||||||
|
each domain:
|
||||||
|
|
||||||
|
- **architecture** — structural commitments (storage, layering,
|
||||||
|
languages, libraries)
|
||||||
|
- **process** — team workflow (commits, branches, releases, reviews)
|
||||||
|
- **design** — user-facing surface (UX, UI, public APIs)
|
||||||
|
- **coding-conventions** — team-internal code shape (style, lint,
|
||||||
|
file layout)
|
||||||
|
- **testing** — quality strategy (coverage, layers, gates)
|
||||||
|
|
||||||
|
You might also want, project-permitting:
|
||||||
|
|
||||||
|
- `accessibility` — if you ship user-facing software
|
||||||
|
- `security` — if you handle user data or network surfaces
|
||||||
|
- `licensing` — if you release open-source or commercial
|
||||||
|
- `documentation` — if user-docs are non-trivial
|
||||||
|
- `deployment` — if shipping is non-trivial
|
||||||
|
- `performance` — if you have perf budgets / SLOs
|
||||||
|
|
||||||
|
<!-- pql:records (auto-generated; do not edit manually) -->
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- _(none)_
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- _(none)_
|
||||||
|
|
||||||
|
## Rejected
|
||||||
|
|
||||||
|
- _(none)_
|
||||||
@@ -46,7 +46,7 @@ class ContainersDatasource {
|
|||||||
final response = await _dio.get<String>(
|
final response = await _dio.get<String>(
|
||||||
'/infrastructure/containers/$id/logs',
|
'/infrastructure/containers/$id/logs',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
if (tail != null) 'tail': tail,
|
'tail': ?tail,
|
||||||
'timestamps': timestamps,
|
'timestamps': timestamps,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
|
|||||||
|
|
||||||
/// Tracks if we've logged the environment API user (log once per session)
|
/// Tracks if we've logged the environment API user (log once per session)
|
||||||
static bool _hasLoggedEnvUser = false;
|
static bool _hasLoggedEnvUser = false;
|
||||||
static String? _envApiUser;
|
|
||||||
|
|
||||||
/// Get the environment API user (available after first load)
|
|
||||||
static String? get envApiUser => _envApiUser;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -157,9 +154,8 @@ class _DashboardContentState extends ConsumerState<DashboardContent> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
environmentAsync.when(
|
environmentAsync.when(
|
||||||
data: (envData) {
|
data: (envData) {
|
||||||
// Store and log user once per session
|
// Log the user once per session
|
||||||
if (!_hasLoggedEnvUser && envData.user != null) {
|
if (!_hasLoggedEnvUser && envData.user != null) {
|
||||||
_envApiUser = envData.user;
|
|
||||||
_hasLoggedEnvUser = true;
|
_hasLoggedEnvUser = true;
|
||||||
debugPrint('Environment API user: ${envData.user}');
|
debugPrint('Environment API user: ${envData.user}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class EntitySection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (trailing != null) trailing!,
|
?trailing,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ void main() {
|
|||||||
const mode = DataGridDataMode.infinite();
|
const mode = DataGridDataMode.infinite();
|
||||||
expect(mode, isA<InfiniteDataMode>());
|
expect(mode, isA<InfiniteDataMode>());
|
||||||
expect((mode as InfiniteDataMode).initialLoad, 50);
|
expect((mode as InfiniteDataMode).initialLoad, 50);
|
||||||
expect((mode as InfiniteDataMode).loadMoreThreshold, 10);
|
expect(mode.loadMoreThreshold, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('infinite mode with custom values', () {
|
test('infinite mode with custom values', () {
|
||||||
@@ -404,7 +404,7 @@ void main() {
|
|||||||
loadMoreThreshold: 20,
|
loadMoreThreshold: 20,
|
||||||
);
|
);
|
||||||
expect((mode as InfiniteDataMode).initialLoad, 100);
|
expect((mode as InfiniteDataMode).initialLoad, 100);
|
||||||
expect((mode as InfiniteDataMode).loadMoreThreshold, 20);
|
expect(mode.loadMoreThreshold, 20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user