7945ad49 | Practice Management backend: Clio-backed Matters seam + routes | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body Task 2 of the Practice Management plan. Live Clio reads only; the sole
stored artefact is the matter_workspace_links id/number pair, whose
migration (20260807_01) is Task 1 and is NOT in this PR - every links
query degrades on 42P01/42703/PGRST204 so this runs safely ahead of it.
- lib/clio/mattersSurface.ts: list (mine = responsible union originating,
de-duped, sorted open_date desc; all = one order=open_date(desc) page),
detail + financials, related contacts, time entries, activity update
(If-Match) / delete, workspace link lifecycle. 60s per-user list cache
keyed user+tab+query+status, 200-entry cap, cleared on activity write.
- routes/clioMatters.ts mounted at /clio-matters behind the research
limiter; requireAuth + asyncHandler throughout, fixed client details +
safeErrorLog, uuid/numeric id guards. Minutes to seconds happens at
this boundary once.
- clioRequest gains an additive headers option for If-Match; caller
headers sit beneath the fixed ones so Authorization / Accept /
X-API-VERSION can never be overridden.
- Selector fixes (docs-verified 06/08 spike, live probe pending):
clio_matter_financials asks billable_matters for its own fields
(the shipped matter{...} brace 400'd every call, owner-reproduced);
clio_find_contact treats primary_email_address as a scalar.
- Lifecycle: account deletion and SAR export cover the links table,
42P01-tolerant.
Probe-gated per the spec's open questions: one 200-row page with honest
counts, billed entries locked AND refused server-side with a fixed 409.
Every read uses the caller's own Clio token; link lookups go through
checkProjectAccess, the choke point that also excludes tombstoned
matters. Redacted money/hours report as hidden, never as zero.
Verified: tsc clean; full vitest 836 passed / 48 files (baseline 757,
79 new); prettier clean on all new files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
20288bac | Address review: PGRST205 degrade, per-user rate limit, header hardening | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body Five should-fixes and six nits from the independent review of #81.
PGRST205 was a real defect. A missing TABLE on Supabase is PostgREST's
PGRST205 (404, answered from its schema cache), never Postgres's 42P01 -
verified empirically against PostgREST 14.16 in a local container (no
credentials involved): missing table -> PGRST205, missing column in a
filter -> 42703, missing column in a write payload -> PGRST204. The
42P01-only degrade would have thrown in production, and would have failed
account deletion / SAR export outright on an unmigrated database. The
code table now lives in a dependency-free leaf module lib/postgrestCodes.ts.
- /clio-matters gets its own limiter keyed on res.locals.userId (IP only
as the pre-auth fallback), inside the router because an app-level
limiter runs before auth. The pilot firm NATs one office IP, so the
shared IP-keyed research bucket would have let a few solicitors take
/companies and /legislation down for everyone. New vars
RATE_LIMIT_CLIO_MATTERS_MAX / _WINDOW_MINUTES, documented in
.env.example and CLAUDE.md. Auth moves to router level so no future
route can omit it.
- headersFor drops any caller header colliding case-insensitively with a
fixed one: object spread is case-sensitive and new Headers() then joins
case-differing duplicates into "Bearer attacker, Bearer real".
- linkWorkspace gains the matter-side pre-check; link candidates are
ordered by created_at with the caller's own preferred; the workspace
side of the conflict gets its own 409 copy.
- Nits: cross-user cache isolation test, etag input guard, created_by
predicate on the unlink delete, uuid guard on getLinkForProject,
lifecycle fixtures corrected, amountsHidden renamed amountsUnavailable
(a null price can mean a rate-less entry, so only quantityRedacted may
drive "Hidden by your Clio permissions" copy).
The first attempt imported the predicate from the feature seam into
userDataCleanup, forming an import cycle through lib/access.ts that broke
an unrelated tombstone test in routes/tabular.test.ts; hence the leaf
module. Lifecycle paths must never import a feature seam.
Verified: tsc clean; full vitest 871 passed / 49 files (baseline 757);
prettier clean on all new and newly-touched files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
9f41ca7f | WIP: Clio matters API client, list, detail and modals | sloth-ninja | 2026-08-07 | ↗ GitHub |
b4f53123 | wip: matters toolbar controls (tabs, status filter, workspace sub-filters) | sloth-ninja | 2026-08-07 | ↗ GitHub |
2a85b7fc | wip: matters tabs render swap + link-to-Clio-matter row action | sloth-ninja | 2026-08-07 | ↗ GitHub |
5e6123d9 | wip: sidebar active state for /matters detail route | sloth-ninja | 2026-08-07 | ↗ GitHub |
5033f2e0 | Practice Management frontend: Clio-backed Matters tabs, matter detail, time entries, workspace links | sloth-ninja | 2026-08-07 | ↗ GitHub |
Task 3 of docs/superpowers/plans/2026-08-07-practice-management.md, built to the
owner-approved mock-up frames A/B/C.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
6a2d57e1 | Merge pull request #81 from Sloth-ninja/pm-backend | Ezana.H | 2026-08-07 | ↗ GitHub |
Practice Management backend: Clio-backed Matters seam + routes |
6614b410 | fix(B1,S4,nits): ClioTimeoutError distinct from abort; scrub real matter name; money 2dp; year-aware short date; hydration-safe tab store | sloth-ninja | 2026-08-07 | ↗ GitHub |
a38be52b | fix(S5,nits): 401 reconnect path to Frame C banner; hydration-safe tab via useSyncExternalStore; new workspace switches to Workspaces tab | sloth-ninja | 2026-08-07 | ↗ GitHub |
b84f601f | fix(S2,S3,S6,nits): errors render in their own cards; unbilled hours stated independently; contacts seq guard; toggle a11y; no edit on redacted own entry | sloth-ninja | 2026-08-07 | ↗ GitHub |
69c981a3 | fix(S1,nits): 400 keeps the edit (only 409/412 offer reload); send only changed fields; correct the 412 prose | sloth-ninja | 2026-08-07 | ↗ GitHub |
4213069f | docs: BUILD_LOG review round for the pm-frontend fix wave | sloth-ninja | 2026-08-07 | ↗ GitHub |
6b52469b | fix(I1): stop offering workspace links that cannot work | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body getLinkForMatter collapsed the "unsupported" sentinel to null, so "this
matter has no workspace yet" and "workspace linking does not exist on this
deployment yet" were indistinguishable by the time they reached the UI. The
detail page therefore drew a "Start workspace" button, and the Matters page a
"Link to a Clio matter" row action, that could only ever answer 409 - the
`matter_workspace_links` migration (20260807_01) is not in the repo at all, so
that is the CURRENT state everywhere, not a hypothetical.
- getLinkForMatter now returns the sentinel; the detail route turns it into an
explicit `linksUnavailable` flag (and still nulls `link`).
- New `areLinksAvailable(db)` probe, fails OPEN on a transient error (the
27/07 rule: availability gate, not a destructive op) - the write path's own
fixed refusal stays the honest backstop.
- The flag rides the LIST payload too: both remaining link affordances live on
surfaces that never load a matter detail.
- Frontend hides both affordances behind it with honest copy ("Workspace
linking isn't available yet."); the picker also refuses rather than letting
a solicitor choose a matter and only then be told.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
a46ca14f | fix(I2): exempt /clio-matters from the IP-keyed general limiter | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body The per-user bucket inside the /clio-matters router was decorative: the
app-level generalLimiter still ran in front of it, keyed by IP, at a shared
300 requests per 15 minutes. The pilot firm NATs its whole office through one
address, so that IP bucket - not the per-user one - was the binding constraint
the moment two solicitors browsed matters, and exhausting it would degrade
every other route for their colleagues.
Exemption is a skip on the prefix (kept where generalLimiter is mounted, so the
JSON body parser and everything after it still apply - mount-order would have
skipped those too). Exact-segment match, not a bare startsWith, so a future
`/clio-matters-anything` cannot inherit the exemption.
Corrects the rationale comments, which claimed matters browsing could exhaust
the "shared research allowance" and take /companies and /legislation down:
/clio-matters was never on researchLimiter - it was on generalLimiter, so the
stated mechanism was wrong even before this change. Fixed in
routes/clioMatters.ts, index.ts, .env.example and the CLAUDE.md registry row.
index.ts retains its pre-existing prettier deviation at makeLimiter (the file
was not prettier-clean at baseline); only the hunks added here are normalised.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
c59291b1 | Merge pull request #82 from Sloth-ninja/pm-frontend | Ezana.H | 2026-08-07 | ↗ GitHub |
Practice Management frontend: Clio-backed Matters tabs, matter detail, time entries, workspace links |
a0c89e55 | fix(I3): carry isOwner on the workspace link so Unlink is not offered-then-refused | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body Unlinking is owner-only and the server enforces it with a 403, but the link
payload carried no ownership signal - so a colleague reading a firm-visible
matter saw an Unlink button and only learned it was not theirs by pressing it.
toLink already had the authorising `checkProjectAccess` result in hand, so
isOwner is derived from that same result and cannot disagree with what the
server will enforce. The two write paths (linkWorkspace, createWorkspaceForMatter)
pass true by construction - both refuse or create for the caller as owner.
The frontend withholds the button rather than disabling it; the handler keeps
its 403 branch as the belt for a payload that went stale under an open page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
d17de50e | chore(i/M13): scrub a real client name from the public repo | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body "Kyckr" is a real client of the pilot firm and this repository is public. It
appeared 11 times in manageTools.test.ts (as search-query fixtures) and once in
BUILD_LOG's 04/08 entry, where it recorded which client a solicitor was
searching for - a confidentiality leak, not a naming nit.
Test fixtures now use the fictitious "Aldergate". The BUILD_LOG line is redacted
IN PLACE as "[client name redacted 07/08]" rather than rewritten, so the entry
stays honest about what it originally said.
Stated plainly: this scrubs the working tree only. The name remains in git
history (commits predating this one) and would need a history rewrite to remove
- out of scope for a fix wave, flagged to the owner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
2bfa0b57 | fix(a/M11): route the limiter's IP fallback through ipKeyGenerator | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body The custom keyGenerator returned req.ip directly, which express-rate-limit
rejects as ERR_ERL_KEY_GEN_IPV6 - confirmed firing in the route suite, not
theoretical. A raw IPv6 address gives every host in a /64 its own bucket, and a
single client can hold trillions of those, so the fallback limit was bypassable.
Kept the fallback rather than deleting it (it is unreachable today - requireAuth
is mounted ahead of the limiter and 401s first - but "unreachable" is a property
of the current mount order, not a guarantee), and recorded that reachability
argument in the comment. Keys are now namespaced user:/ip: so a user id can
never collide with an address literal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
d7091d34 | refactor(c/M12): one UUID_RE, and put the numeric-id docblock on isClioId | sloth-ninja | 2026-08-07 | ↗ GitHub |
commit body Two defects in the same few lines. The docblock describing numeric Clio ids sat
above UUID_RE, which is neither numeric nor Clio's - the function it documents
(isClioId) had none, so the comment described the wrong constant.
UUID_RE also existed twice, once in the seam and once in the routes, guarding
the same values. Duplicated validation regexes drift, and the copy that drifts
is the one that stops guarding. The seam now exports it and the route imports it.
No behaviour change; the two suites over these files stay green (90 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
300ee296 | fix(M5): never guess a currency symbol on a money figure | sloth-ninja | 2026-08-08 | ↗ GitHub |
commit body formatMoney defaulted a missing currency code to GBP, so an amount Clio
reported without a currency rendered as "£1,240.00" - a specific claim about a
figure on a legal bill that nothing supported, and simply wrong on a matter
billed in euros.
An absent or unrecognised code now renders the number bare (still 2dp, so
columns line up), never dressed in a symbol. Same honesty rule the redaction
flags already encode: a value we cannot substantiate is not presented as one we
can. Null stays an em dash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
a86e6867 | fix(f/M4,M1,M7): wire the link lookup; stop blaming Clio for our 429s; treat 404 as gone | sloth-ninja | 2026-08-08 | ↗ GitHub |
commit body Three reviewer items that land in the same files.
(f/M4) GET /clio-matters/links/:projectId now exists, using the getLinkForProject
export that was dead code. Registered in the literal-path block ahead of the
/:matterId routes - declared after them, "links" would be swallowed as a matter
id (asserted by a test). Uuid-guarded in the route as well as the seam, and one
uniform 404 for every kind of "no" (absent / not visible / malformed /
unmigrated) so the endpoint cannot be used to probe which workspaces exist. The
Workspaces-tab badge that would consume it stays deferred.
(M1) A 429 can come from our own per-user bucket, but the server called it "Too
many Clio requests" and the client overwrote it with "Clio is rate-limiting
requests" - both blaming Clio for a limit JessicaOS imposed. The server detail
is now neutral, and the surfaces show the server's own message on 429 with a
neutral fallback, so a real Clio 429 and ours stay distinguishable.
(M7) A 404 on save or delete means the entry is already gone from Clio (deleted
in another window), which makes the list on screen stale - so 404 joins 409/412
in offering "Reload time entries", and a 404 delete drops the row rather than
leaving a ghost the solicitor can retry forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
57d58714 | docs(j/M10) + fix(M8): document the clioState gates; stop the initial-tab flash | sloth-ninja | 2026-08-08 | ↗ GitHub |
commit body (j/M10) The three clioState gates are deliberately asymmetric - fail-closed for
the link WRITE affordance, fail-open for the READ tabs, and narrowest of all for
the banner, which makes a positive claim about the user's account. That was
three scattered half-comments that read like inconsistency. Kept as they are
(the asymmetry is correct per the 27/07 rule) and explained in one block naming
"unknown" as the value that separates them.
(M8) With no stored preference the page resolved to Workspaces while the Clio
status was still loading, painted that list, then jumped to My matters the
moment the status landed. The tab decision now waits for a definite status, the
default matches the tab bar's existing optimism instead of contradicting it, and
the body renders a skeleton until then - so no wrong list is ever painted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
005ea7e5 | docs(M3): commit the Practice Management spec and state per-viewer workspace semantics | sloth-ninja | 2026-08-08 | ↗ GitHub |
commit body Two things here, and the first was not in the brief.
The spec was NEVER COMMITTED. CLAUDE.md, BUILD_LOG and three source files all
cite docs/PRACTICE_MANAGEMENT_SPEC.md, and every sibling spec (FIRM_LIBRARY,
TABULAR_TEMPLATES, DELETION_GOVERNANCE) is tracked - this one existed only as an
untracked file in the owner's checkout, so the whole train's design document was
one `git clean` from gone and invisible to reviewers. Committed as-is apart from
the sentence below.
(M3) Surfaces section 4 now states the per-viewer semantics explicitly: the
workspace a solicitor sees against a Clio matter is one they can access, so a
colleague's appears only when firm-visible. Two people on one matter seeing
different workspaces (or one and none) is correct, not a sync bug - worth
writing down before someone "fixes" it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
3cdc1eab | docs: BUILD_LOG entry for the Practice Management fix wave | sloth-ninja | 2026-08-08 | ↗ GitHub |
commit body Scope, per-item changes, verification evidence, the contention note, and the
items flagged to the owner (missing 20260807_01 migration; "Kyckr" surviving in
git history; the four reviewer-deferred items).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
9c6cd9f0 | Address review B1: do not respell the scrubbed client name in the scrub's own record | sloth-ninja | 2026-08-08 | ↗ GitHub |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
52e9e0bf | Merge pull request #83 from Sloth-ninja/pm-train-fixes | Ezana.H | 2026-08-08 | ↗ GitHub |
PM train: composed-range fix wave |
39e629f2 | feat(migration): add matter_workspace_links (Practice Management Task 1) | sloth-ninja | 2026-08-12 | ↗ GitHub |
commit body Creates the one stored Clio artefact for Practice Management: the
id/number pair anchoring a JessicaOS workspace (a projects row) to a
live Clio matter. Everything else stays a live Clio read; no matter
data is persisted. project_id's on delete cascade is load-bearing for
the WS8 deletion-governance purge path. Mirrored in schema.sql
alongside the user_clio_connections precedent, RLS enabled, revoked
from anon/authenticated.
Includes the owner's own allowlist entry for
20260807_01_matter_workspace_links.sql in
.claude/hooks/authorized-migrations.json (committing the owner's
edit, not authoring it, per CLAUDE.md hard rule 1).
The merged train's seam (lib/clio/mattersSurface.ts,
userDataCleanup.ts, userDataExport.ts) already tolerates this table's
absence, so this is additive-only: zero behaviour change until this
migration is actually run in production.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
c53b333a | docs: commit the Practice Management plan doc | sloth-ninja | 2026-08-12 | ↗ GitHub |
commit body docs/superpowers/plans/2026-08-07-practice-management.md was sitting
untracked in the owner's checkout, tracking Task 1 (this migration)
of the Practice Management train. Committed as-is.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
489da5e8 | docs: BUILD_LOG entry for the matter_workspace_links migration | sloth-ninja | 2026-08-12 | ↗ GitHub |
commit body Records Practice Management Task 1: scope, the load-bearing cascade
FK decision, RLS/revoke posture, and the four re-run test suites'
pass counts proving the pre-migration degrade direction still holds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
d3bf3ebb | Merge pull request #84 from Sloth-ninja/pm-migration | Ezana.H | 2026-08-12 | ↗ GitHub |
Practice Management Task 1: matter_workspace_links migration + plan doc |
0cbd065b | docs: CLAUDE.md status - Practice Management shipped, pilot-feedback wave live | sloth-ninja | 2026-08-14 | ↗ GitHub |
commit body Practice Management surface is ACTIVE in production: #84's migration
20260807_01 ran and was verified by information_schema (7 columns, exact
match), backend Fly v22 + frontend Worker deployed 13/08 with the
Cloudflare purge, owner QA passed on the live Matters tab. The 13/08
pilot-feedback wave (#85 backend, #86 frontend) shipped as Fly v23 and
QA'd all-pass 14/08. Backend suite count refreshed to 888 tests / 50
files. Records what is still open: the unrun Clio live probe and its
prerequisites, the deferred post-migration pass, the three open owner
decisions, the proposed-but-unsigned-off stop-check hardening, and the
Find Case Law computational-analysis licence application.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|