ac890fd996cde9a4afa545434cf9ebfec0c22a80
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
500c2f2179 |
name the six over-long foreign keys, so a push can honestly be a no-op
Drizzle generates foreign-key names past Postgres's 63-character identifier limit. Postgres stores the truncated form, drizzle keeps comparing against its own longer one, and every `db:push` on every host drops and re-adds constraints that are already identical. Eight statements, for ever. Documented in src/databases/CLAUDE.md on 2026-08-19 and deferred as "noise rather than a fault". What ended that was last night's deploy plan saying "expect no changes, stop and report if it wants to alter something" — which this made impossible to satisfy on any host, so the deploy agent had to overrule its own instructions and say why. An annoyance had become an instruction nobody could follow. Two of the six are in this repo; the other four are in the atelier and bitcoin-wallet plugin repos and are committed there. Verified the way the doc prescribes: push twice against a scratch database, and the second run went from ten statements to two. The two that remain are `pk_music_now_playing`, and that one is tested rather than assumed. Removing its explicit name produces the identical churn under drizzle's generated name, so it is the named-composite mis-diff and not the truncation one, and a primary key cannot escape to an index form. A push now prints exactly those two lines — which is a far more useful baseline than ten, because anything else really is your change. Naming a foreign key means removing the inline `.references()` or drizzle creates both, and `walletChainCache` had to move to the three-argument `pgTable` form because there was nowhere else to put the declaration. Both noted in the doc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
46d621c91b |
chat notifications reach the phone, on the same rule as the bell
the browser bell and phone push are now two subscribers to one signal. both
ask shouldNotify(); neither owns the rule.
deliberately a second subscriber rather than a branch inside notify-delivery.
the two channels share only the question "does this account want to hear about
this" — the browser needs an open tab and sends the chat's real title over a
loopback socket on the owner's own tailnet, the phone needs no tab and must
send NO title, because that hop goes through apple. one function doing both
means an `if (isPush)` at every step, and the first thing to go wrong is a
title leaking because the branch was missed in one place.
two codes, officer/core:chat.turn-ended and chat.input-needed. the route is
written out with its scheme rather than derived from the target: the hub has
no chat screen — chat is Butler, a sibling app — so these deep-link OUT over
officer-chat://, and two of the suite's twenty-one schemes already break the
officer-<app> pattern (officer-videos, officer-readaloud). deriving that works
until it silently does not.
`thread` interpolates now, so `chat:{id}` groups per conversation. a static
thread forces a choice between "three pings from one chat collapse" and "two
busy chats stay apart", and the wrong answer is that every chat is the same
chat.
dropped officer/core:agent: chat.turn-ended supersedes it and its route
pointed at officer://chat/{id}, a hub screen that does not exist.
the banner cannot name which conversation. a chat's title is content and
content does not travel; it says what happened, and tapping opens the chat,
which is where the name lives.
|
||
|
|
595dd082a7 |
delete Task Logs
A complete read path over a table nothing could write to. task-logger.ts exported createTaskLog, appendToLog and finalizeLog, and none of the three was called anywhere in the tree — so `task_logs` could never gain a row, and the screen was permanently empty for everyone. The read half was fully wired: mounted router, capability claim, dock icon, two routes and a page-title rule. That is why it looked alive. Gone, in the order it was reached: Screens/Dashboard/TaskLogs/ the screen App.tsx /task-logs and /task-logs/:id Dashboard/index.tsx the export Layout/Dock.tsx the 'Logs' icon, and ScrollText with it state/usePageTitle.ts the title rule api/task-logs/task-logs.ts the router, and its mount in hono.ts api/task-logger.ts 101 lines of orphaned writer officer_db/src/operations/ the directory officer_db/src/schema.ts the export line officer_db/src/types.ts TaskLogSelect / TaskLogInsert capabilities/registry.ts loses '/task-logs' from the `tasks` capability's `api` AND `routes`. The api half is not optional: assertCapabilityTotality check 2 refuses to boot on a capability claiming a prefix nothing mounts, so unmounting the router while leaving the claim would have stopped the server starting. db:push now creates 21 tables, down from 43 at the start of the evening. officer_db/src/operations was one of the two lopsided directories the schema/queries merge exposed. integrations/ is the remaining one, and it is legitimate — it spans server and user-data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
288b4bf683 |
each feature declares its own exports; the barrel is one line per feature
src/index.ts is 47 lines and reads as a list: `export * from './agent-panels';` and 22 more, under `db` and `schema`. It was 297 lines of hand-written named exports. What a feature exports now lives in <feature>/index.ts, beside the schema and queries it describes. Adding a query function is one file in one directory rather than that file plus a list three levels up that nothing enforces — a function missing from that list was invisible to all 107 consumers while existing and compiling perfectly. The old file had drifted in the ways a hand-maintained list does: twelve features were listed twice because values and types were separate statements repeating the path, soulseek three times, two features used an inline `type` specifier instead, and `db` and `schema` — the package's most fundamental exports — sat at line 270 with notify, types and app-store appended after them. The surface is byte-for-byte the same set. Checked rather than asserted: 247 exported names before, 247 after, no missing and no extra. The per-feature index files carry the same named lists the barrel did, so `export *` widens nothing. `operations` is deliberately not in the list, and the root file says why: it has a schema and no queries, its task_logs is reached as `schema.taskLogs` from src/servers past this package's boundary, and its other two tables are read by nothing at all. Verified: every file in the package parses, every relative import resolves to a real file, and all 107 consumers still parse. Not typechecked — empty node_modules, frozen installs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
68f2c55ecf |
one directory per feature: schema.ts and queries.ts together
src/databases/officer_db/src/<feature>/{schema.ts,queries.ts}, replacing the
parallel schema/ and queries/ trees. 24 feature directories, 46 files moved with
git mv so history follows.
The parallel trees had drifted, which is what the restructure is really fixing:
four features were named differently on each side — app-store/sidecar-installs,
email/email-accounts, server/server-config
operations had a schema and NO query file: its task_logs is reached directly
from src/servers/api/task-logger.ts, bypassing this package's own boundary
integrations had queries and NO schema, because it spans two features'
tables — server_integrations and user_integrations
Both lopsided cases survive as directories holding one file, which states the
problem instead of hiding it across two trees.
Nothing outside the package changed how it imports. `officerdb`, `officerdb/types`
and `officerdb/db` resolve exactly as before; index.ts absorbed the path changes.
Added `"./*": "./src/*"` so the new layout is reachable — `officerdb/soulseek/schema`
— which one script needed, because soulseek is a plugin and therefore commented
out of the aggregator.
schema/index.ts became src/schema.ts, keeping the core/plugin split from earlier
tonight. drizzle.config.ts and the package's "./schema" export follow it.
Verified rather than assumed: all 52 files in the package parse, every relative
import resolves against the new layout (checked by walking each specifier to a
real file, since parsing does not check paths), and everything in the tree
importing officerdb still parses. Not typechecked — empty node_modules, frozen
installs.
One rewrite bug worth recording: the rule mapping a query module's sibling import
also matched the './schema' this pass had just written, turning it into
'../schema/queries' in 22 files. Caught by the resolver check, not by parsing —
both spellings parse fine.
Also corrects every path reference the move invalidated: src/databases/CLAUDE.md's
layout diagram, the root CLAUDE.md data section, three docs, and seven sidecar
comments naming queries/<x>.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a9b6494f56 |
db: stop push re-creating every composite key on every run
`bun db:push` planned 32 statements against a database that already matched the
schema, and stopped on a "do you want to truncate screens?" prompt that answering
could not resolve — the same question came back next run. Root cause found and
fixed rather than worked around.
drizzle-kit mis-diffs named composite unique CONSTRAINTS. It reads one back,
compares it against a schema declaring the identical name, columns and order,
decides they differ, and emits DROP + ADD. Fifteen of those, forever. Reproduced
on a database drizzle had itself created seconds earlier, so it is not drift.
Single-column .unique() is diffed correctly; only unique('name').on(a, b) is
affected. Unique indexes go through a different code path and are stable, so all
fifteen are now uniqueIndex.
A unique index enforces exactly what the constraint did — verified, a duplicate
insert still fails on uq_screens_user_name — and onConflictDoUpdate accepts it as
an arbiter. It cannot be a foreign-key target, but nothing here targets a
composite key; checked before converting.
Separately, user_integrations_server_integration_id_server_integrations_id_fk is
65 characters and Postgres truncates identifiers at 63, so drizzle compared its
generated name against the stored, truncated one and re-created the FK every run.
Declared explicitly as fk_user_integrations_server_integration.
Measured on a scratch database, pushing twice each time:
before 32 statements, interactive prompt
after uniqueIndex 4
after FK fix 2
The two that remain are a composite primaryKey with the same bug and no index
form to escape to — music_now_playing re-creates pk_music_now_playing every push.
Silent, no prompt even with rows, data unaffected, and naming it explicitly does
not help. Documented as expected.
The conversion itself was tested against populated tables, since that is what the
real database will do: no prompt, and all rows survived.
Docs rewritten in src/databases/CLAUDE.md — the rules committed an hour ago
described the broken behaviour and would have been wrong the moment this landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6af80a20b0 |
db: add the user role column, and write down how push actually behaves
Adds `role` to users as a text column with a TS enum, matching how `status` is
done — no pgEnum, so adding a role stays a one-line schema change rather than an
ALTER TYPE while the set is still settling. USER_ROLES and UserRole are exported
from officerdb so the API and UI enumerate them from the column definition rather
than a second hand-written list. Defaults to 'Member', the least privileged, so a
row created by a path that does not think about authorisation cannot mint an
admin. NOT YET PUSHED — the column is in the schema, not in the database.
The larger half of this commit is a rule for anyone running `bun db:push`,
because the first time you run it, it looks like your change broke something.
It plans 16 statements against a database that already matches the schema, and
plans them again on the next run. Two separate causes, both diagnosed here:
- it drops and re-adds every NAMED COMPOSITE unique constraint — all 14, from
uq_screens_user_name to uq_wallet_labels_wallet_kind_ref. Single-column
.unique() diffs correctly and is untouched; only unique('name').on(a, b) is
affected. Names, columns and order in the database are identical to what the
schema declares. drizzle-kit 0.31.9 / drizzle-orm 0.45.1.
- it drops and re-adds one foreign key because
user_integrations_server_integration_id_server_integrations_id_fk is 65
characters and Postgres truncates identifiers at 63, so drizzle compares its
generated name against the stored, truncated one and always differs.
The rules, ranked by how much damage getting them wrong does: never answer "Yes,
truncate the table" — it destroys rows and does not help, since the constraint is
re-added next push either way; never delete a constraint from the schema to
silence the prompt, because upserts depend on it existing; do not reach for
--force until someone has established on a scratch database which branch it takes.
Run with --verbose first and read what it is actually planning.
Pointers added to platform/CLAUDE.md and the workspace root CLAUDE.md, since
those are what a session reads before it ever opens the database directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
db9d17d6fe |
docs: the convention docs were describing a different codebase
Second pass. These three are the ones a new contributor reads first, and all three were teaching things that are not true here. src/databases/CLAUDE.md claimed "Three PostgreSQL databases", listed two, and there is exactly one. Its type examples were Screenshot / Experiment / Company / GanOauth — none of which have ever existed in this repo; it had been carried over from another project wholesale. Rewritten against the real schema, queries and types, and it now carries the two things that actually bite: push-not-migrations, and the rule that the schema is the source of truth for what the database may CONTAIN, not just its shape — with the sql.raw trap in check() written down, since getting it wrong breaks push for the whole schema. src/apps/CLAUDE.md had the same problem in its examples (useExperimentsList, ExperimentCard, a state/ directory layout that does not exist), listed a `useWebsockets` hook that is not there while omitting useChatWebSocket, usePanelChannel and useJobs, and closed with links to three app docs that have never existed. Examples now use real hooks, and it points at the navigation audit — a frontend doc that did not mention the one rule the platform CLAUDE.md calls authoritative was a real gap. CONVENTIONS.md said, in bold, that useMemo and useCallback are "strictly prohibited" because "React 19's compiler handles memoization automatically". Wrong twice: the React Compiler is an opt-in build plugin that is NOT installed here, so React 19 memoizes nothing on its own — and roughly 40 files use each hook regardless, including code added this week. Replaced with guidance that matches both reality and the actual tradeoff, and says plainly what it used to claim. A rule that is false and universally ignored makes every other rule in the file look optional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9ab0940ca4 | first |