Phase A.0 revealed Tailscale SSH on mRiver intercepts :22 from tailnet
peers and bypasses OpenSSH's authorized_keys entirely (banner
"SSH-2.0-Tailscale", auth method "none", command= restriction never
fires). The fix is port 22022 via a systemd ssh.socket drop-in:
Tailscale SSH only intercepts :22, so :22022 hits real OpenSSH where
the design's command=/from= shim restriction works as specified.
Updated:
- §3 locked decisions: row 5 added (port 22022, m's call 23:35)
- §4.5 new subsection: Tailscale SSH bypass via socket drop-in
+ records the "Address already in use" first-attempt failure as a
"don't retry without cleaning sshd_config Port directives first"
lesson
- §5.2/5.3: ssh-keyscan now uses -p 22022; known_hosts is host:port
keyed for non-22 ports
- §6.1/6.2/6.3: SSHPort field on RemotePaliadinService config, -p
flag in callShim, PALIADIN_REMOTE_PORT env (default 22022)
- §7 phasing: A.0 completion checked off step-by-step with concrete
fingerprints; A.5/A.6/A.7 split out as m-driven
- §8 security: Tailscale-SSH-on-:22 risk explicitly tabled with
port-22022 mitigation
- §10 deliverables: mRiver host-setup artifacts noted
- §12 new Phase A.0 completion summary with the three secrets m
needs to register in Dokploy
Phase A.0 verified end-to-end:
- ssh -p 22022 paliad-prod-key m@mriver health → ok
- run-turn UUID base64msg → 3.4 s including a real Claude response
- from="100.99.98.201" correctly rejects connections from mRiver
itself
mRiver host state in place (not in repo): authorized_keys with
restrictions, /home/m/.local/bin/paliadin-shim, ssh.socket drop-in.
Three secrets staged at ~/.paliad-staging/ on mRiver for m to copy
into Dokploy: paliad-prod-key (PALIADIN_SSH_PRIVATE_KEY),
known_hosts (PALIADIN_KNOWN_HOSTS), and the three plain env vars.
Refs m/paliad#12
Inventor design for routing Paliadin from paliad.de's Dokploy container
on mLake to mRiver via Tailscale + SSH, preserving m's Claude Code
subscription instead of paying Anthropic API tokens.
Three sub-designs covering m's four locked decisions (2026-05-07 22:35):
- network_mode: host on paliad (m overrode the sidecar recommendation;
Phase A explicitly tests traefik compatibility under host mode)
- server-side paliadin-shim with one RPC per turn (run-turn / reset /
health / bootstrap), authorized_keys command= restriction, from=mlake
- env-var routing trigger (PALIADIN_REMOTE_HOST) + Paliadin interface
split: LocalPaliadinService keeps the laptop PoC, RemotePaliadinService
shells out to ssh m@mriver paliadin-shim
- ed25519 keypair via Dokploy secret PALIADIN_SSH_PRIVATE_KEY, written
to a chmod 600 tmpfile at startup; pinned host key via
PALIADIN_KNOWN_HOSTS
Verified live before designing: mRiver tmux+claude present, mLake
Tailscale active and sees mRiver, paliad Dockerfile is alpine-minimal,
no authorized_keys on mRiver yet. No assumptions left from CLAUDE.md.
Includes: friendly error code mriver_unreachable extending t-paliad-150,
single-flight rate limit, security review (defence-in-depth via
command=/from= restrictions), three-phase rollout (manual proof →
Dockerfile bake → polish), file-level deliverables for the coder shift.
Inventor stops here — no code shipped. Awaiting m's go/no-go.
Refs m/paliad#12
058 = paliadin_poc (t-146), 059 = profession_vs_responsibility (t-148), both shipped on main 2026-05-07. Next available is 060.
Per maria's coder-shift instruction.
Three view modes (Tree default | Cards | Flat), chip filter row, pinning,
search-as-tree-filter, mobile drill-in. Cards view (m's addition) has
configurable content + per-user prefs in localStorage v1.
Q15 decision (delegated to inventor): bespoke /projects, NOT Custom Views.
Custom Views is event-shaped; projects are scope, not events. Adding
SourceProject + ShapeTree to t-144's substrate would break shape ⊥ source
orthogonality. Reversible if a unifying abstraction emerges.
Two-PR phasing: PR 1 = tree + chips + pin + search (~1100-1400 LoC,
migration 058). PR 2 = Cards view + customisation modal + cards-preview
endpoint (~700-900 LoC, additive on PR 1).
4 surfaced questions for m via AskUserQuestion: default landing + view mode,
cards default content, cards customisation scope, search shape. Other 17
questions answered with recommendations + rationale per the dogma (make it
easy for m).
Awaiting m's go on §12 questions before locking. NO coder shift until lock.
m's reframing 2026-05-07 20:56: Paliadin is "mostly for myself now
but can be expanded — monitoring use." Two-stage shape replaces the
single-PR production-v1:
- Phase 0 (PoC): tmux-Claude pattern lifted from goldi/mVoice
(mVoice/server.py:250-380). Claude Code window in a long-lived
tmux session, prompts via tmux send-keys -l, response via
/tmp/paliadin/{turn_id}.txt tail-f → SSE relay. Single user (m),
m's laptop only (PALIADIN_ENABLED=false on prod). ~600-900 LoC,
~1 day. Migration 057 (PoC variant) stores full prompt + response
for monitoring — no redaction at this scope.
- Phase 1 (production v1): the original §2-§6 Anthropic API design,
GATED on PoC success per §0.5.7 expansion criteria (≥3 turns/wd,
≥50% tool-use rate, 4 weeks).
§0.5 (new) inserted as the load-bearing PoC spec. §7 leads with the
two-stage frame. §8.5 questions split into PoC-relevant (Q-PoC-1..6)
and production-v1-deferred. youpc case-law lookup promoted to
Q-PoC-6: m himself does case-law research, so include it from day
one (cross-schema SELECT into data.judgments is technically trivial
since paliad and youpc share the same Postgres).
What we drop for PoC: Anthropic API client, BYO-AI, rate limit,
token caps, multi-user RLS edge cases, /admin cost dashboard,
compliance disclosure, most i18n keys.
What we keep: system prompt voice, citation discipline (best-effort),
visibility gate (Claude is required to use paliad.can_see_project()
in queries), /paliadin surface, SSE shape, audit table.
The two-stage shape protects against the t-145 pattern: ship cheap,
observe, decide. No 4500-LoC investment based on m's gut feel about
adoption.
Inventor design pass for the Paliadin: a Claude-backed conversational
assistant grounded in the user's own paliad data + paliad's static
reference (courts, glossary, deadline rules, Fristenrechner concept
tree). Long-lived in-process Go service that calls Anthropic's
Messages API directly with tool use; every tool is a thin shim over
an existing service (Dashboard / Project / Deadline / Appointment /
Court / Glossary / DeadlineRule). RLS / visibility inherited from
those services — Paliadin literally cannot see what the caller cannot.
Five coordinated sub-designs answer the issue's 20 open questions:
A. LLM architecture + tool-use + prompts (§2)
B. Data access + RLS + PII (§3)
C. UX (§4)
D. Token budget + cost + audit (§5)
E. Phasing (§7)
Phase 1 v1: /paliadin full page + sidebar entry, SSE stream of
Anthropic, 7 read-only tools, session-only history, 30/hour user cap
+ 1000/hour global cap, audit row per turn (metadata only — no
transcript), 4k input + 2k output token caps, no avatar/mascot, no
proactive onboarding. Migration 057 introduces paliadin_turns +
paliadin_rate_limit. Single PR, ~3500-4500 LoC.
mlex / /lex-* reuse: shape (system-prompt voice, tool-catalog idea,
citation style) — NOT code. mLex is a workspace, not a Go/TS repo;
the /lex-* skills drive Claude against youpc's MCP and cannot be
embedded in a paliad service.
Premise verifications surfaced one CLAUDE.md doc-bug (the
ANTHROPIC_API_KEY "Reserved for Phase H — do not set" row needs to
flip in the implementation PR — Paliadin un-defers it).
12 open questions for m in §8.5 — Anthropic key choice (personal vs
HLC enterprise), default model (Sonnet vs Haiku), surface
(/paliadin page vs drawer), mascot phase, 2-PA sanity check before
locking scope, etc. Same adoption-risk concern that just parked
t-paliad-145 — Paliadin's edge over open-Claude-in-another-tab is
data grounding, which only works if v1 makes it visible (citation
chips + tool-call evidence + tagline).
STOP after design. Awaiting m go/no-go before coder shift.
Inventor design doc (kepler) for issue m/paliad#6. Splits the conflated
project_teams.role column into two axes:
- paliad.users.profession (firm-wide, drives t-138 approval ladder)
- paliad.project_teams.responsibility (per-project, lead/member/observer/external)
Approval ladder evaluated as tuple: profession_level if responsibility
opens the gate (lead/member), else 0. Policy grammar from t-138 stays
single-valued.
Verified live state: project_teams=3 rows (all 'lead'), partner_unit_members=20
rows (all default 'attorney'). Backfill is essentially trivial; risk is the
SQL rewiring (4 sites in approval_service.go, 2 in derivation_service.go,
2 in reminder_service.go) — all mechanical.
12 open questions from issue body answered with recommendations + rationale +
alternatives. Awaits m's go before any coder shift.
DESIGN READY FOR REVIEW.
m's lock-in 2026-05-07: agree with all recommendations on Q1-Q18 and §10
Q19-Q27, with one correction on Q4: "activity" is a content selection
(sources + filters), not a render shape. Folded into `list` shape with
density: "compact" + actor/time columns. Shape ⊥ source — any source can
render in any shape.
Render shapes for v1: list / cards / calendar (3, was 4).
PR split decision (delegated to inventor): A1 backend substrate + API
(no UI change, ~1800 LoC, smoke via curl) → main → A2 frontend Custom
Views UI (~1600 LoC, additive on A1) → main.
Status flipped DRAFT → LOCKED. Inventor → coder transition initiated.
m's go/no-go pass at 2026-05-06 15:58: "I agree with all your recommendations
- go." All 19 questions in §6 lock as the recommended answers verbatim.
§0 status flipped from READY-FOR-REVIEW to LOCKED. New "Locked m decisions
on §6" subsection captures the highlights inline so future readers don't
have to scan the whole table to know what's pinned.
§13 end-of-design line updated to reflect the lock.
Implementation phasing (§7) unchanged:
- Phase 1: bug fix on the 3 narrow service methods (no schema, ~400 LoC,
ships standalone, closes the user-visible /projects/{id} "Keine Fristen"
bug).
- Phase 2: migration 055 (partner_unit_members.unit_role,
project_partner_units, extended can_see_project()) + DerivationService +
frontend Team-tab subsections + /admin/partner-units unit_role tagging
+ project /settings/team Partner Units section. Independent of t-138.
- Phase 3: approval extension — canApprove + inbox SQL widening for
derived_peer decision_kind. Gates on cronus's t-138 (currently on
mai/cronus/inventor-dual-control @ b3401ec) landing on main.
Inventor parked. Awaiting head's coder-shift assignment.
Three coordinated sub-designs in one doc, scoped to m's locked constraints
(2026-05-06):
1. Surface-by-surface aggregation policy. Bug surface fix:
/projects/{client_id} renders "Keine Fristen" because
DeadlineService.ListForProject + AppointmentService.ListForProject +
ProjectService.ListProjectEvents all WHERE project_id=$1 exact-match
instead of walking paliad.projects.path descendants. The shipped t-124
contract (projectDescendantPredicate, deadline_service.go:133 etc.)
already aggregates correctly on the union endpoints — three legacy
narrow paths just bypass it. Per-surface decision table for events /
deadlines / termine / Verlauf / project tree counts / dashboard /
CalDAV / email / search.
2. Effective-team semantics. Three structural gaps in the issue's
premise (verified against schema):
- No project↔unit junction (partner_unit involvement on a project).
- No PA/lawyer distinction in partner_unit_members (no role column).
- No lawyer↔PA pairing anywhere — Q11's "where is it stored" → nowhere.
Proposes:
- paliad.partner_unit_members.unit_role (lead|attorney|senior_pa|pa|paralegal),
unit-scoped not firm-wide so 3-axis principle holds.
- paliad.project_partner_units junction with derive_unit_roles[]
(default {pa, senior_pa}) + derive_grants_authority bool.
- Compute-on-read derivation via extended can_see_project() — no
materialised state, no drift.
- Display-effective vs visibility-effective team are different sets;
rename ListEffectiveMembers to ListVisibilityEffectiveMembers + add
ListSubtreeMembers.
3. Approval policy × hierarchy × derivation. Coordinates with t-138
(cronus, mai/cronus/inventor-dual-control @ 7d1ddb9):
- Q10: keep cronus's no-auto-inheritance, harden UX with a "Eltern-
Politik (zur Information)" panel showing parent rules without
applying.
- Q12: derived members visibility-only by default; per-(project, unit)
opt-in flag derive_grants_authority. When opted in, decision_kind
extends with derived_peer for honest audit chronology.
- canApprove + inbox SQL extension shape spec'd; coordinates with
cronus's t-138 §3.4 / §7.4.
Locked m decisions surfaced in §0:
- Behaviour is surface-specific.
- Effective Team of a Client = direct ∪ descendants ∪ partner-unit-derived.
- PA derivation = unit-on-project trigger.
- Derivation honesty: annotated everywhere.
- paliad-only scope.
19 design questions with proposed answers in §6 for m to lock. Migration
055 specced (§5). Implementation phased into 3 PRs (§7) — Phase 1 bug fix
ships standalone if m wants quick win.
Inventor parked. Awaiting m go/no-go before coder shift.
Locked design for 4-Augen-Prüfung on Fristen + Termine. m-confirmed
decisions on all 11 open questions:
- Qualification gate reuses paliad.project_teams.role per-project
(no new firm-wide axis). Adds new value `senior_pa` to the enum.
- Strict ladder: lead > of_counsel > associate > senior_pa > pa.
Default required_role = associate. Per-project override allows pa-
approves-pa or senior_pa-tier escalation.
- Per-(project, entity_type, lifecycle_event) policy grammar — up to
8 settable rows per project in paliad.approval_policies.
- Edit-trigger allowlist = date-bearing fields only (Frist due_date /
original_due_date / warning_date; Termin start_at / end_at).
- Write-then-approve: row mutates immediately, approval_status flips
between approved/pending/legacy. Delete is the one stage-then-write
exception (hard-delete on approve, restore on reject).
- Refuse + global_admin override on single-qualified-approver deadlock.
- Pending state visualised everywhere — list views, agenda, dashboard
traffic-light, project detail, CalDAV-synced calendars (`[PENDING] `
title prefix), email reminders.
- Bell + /inbox page with two tabs (zur Genehmigung / meine Anfragen).
- Operational paliad.approval_requests + audit lifecycle written to
existing paliad.project_events (4 new event_types per entity).
- RLS = same can_see_project predicate; service layer enforces the
approve/reject action gate. CHECK constraint blocks self-approval.
- Mark-legacy backfill: approval_status='legacy' on existing rows;
next mutation flows through the gate.
Implementation phasing: single migration 054 + 8-commit PR plan
covering schema, service, wiring, policy authoring page, inbox,
pending pills, CalDAV/email integration, Verlauf rendering.
Inventor parked. Awaiting m go/no-go before any coder shift.
Archives m's locked design call (2026-05-05 18:51) plus live-codebase
verification: paliad.holidays.country exists per-country; paliad.courts
does not (must create); proceeding_types.jurisdiction is regime not
country (do not remove); 41 hand-curated courts already in
internal/handlers/courts.go ready to seed; HolidayService.loadYear is
country-blind today (latent bug); germanFederalHolidays merge is
hardcoded (must become country-conditional). Task stays ON-HOLD until a
non-DE forum or EPO closure-day calendar comes into scope.
v4 addresses three concerns from m on 2026-05-05 in priority order:
1. Card-click → compute deadline → add-to-project (v3 cards were dead-ends).
2. Filter narrowing bug — slug → concept_id allow-list dropped per-leaf
proceeding_type_code, so picking "UPC infringement opposing party"
leaked DE/EPA/DPMA pills. Confirmed via DB query: 25+ leaves overbroad.
3. RoP-rigorous tree audit: 6 confirmed seed errors (Hinweisbeschluss
DE_INF mismap, notice-of-defence-intention UPC_INF mismap, three
cost-appeal notice-of-appeal mismaps, request-for-discretionary-review
needs UPC_APP_ORDERS narrowing), plus reply-to-cross-appeal coverage
gap and bescheid-mit-frist orphan.
Plan splits into three independent phases (A: filter fix, no schema; B:
card-click flow + new calculate-rule endpoint; C: taxonomy migration 052
without RAISE EXCEPTION coverage gates per last night's outage lesson).
Inventor → coder gate held: no production code in this commit.
m approved all 12 open questions in one batch. Locked spec:
1. Legacy tabs RETIRED in Phase E.
2. Decision-tree depth UNLIMITED (was: 4 max). Property of
event_categories data, not hard-coded.
3. Clickable breadcrumb for navigation.
4. Partial-path bookmarks (?b1=...).
5. Multi-select forum filter, default 1 selected.
6. Path-matching cards at each step. Renamed "Pfad lockern" →
"Schritt zurück".
7. Emojis only, no separate colour treatment.
8. Forum buckets simplified to 10: UPC CFI + UPC CoA + DE LG/OLG/
BGH/BPatG + EPA Erteilung/Einspruchsabt./Beschwerdek. + DPMA.
m collapsed UPC LD/CD into UPC CFI (rules identical).
9. B1↔B2 share filter state.
10. Single branch / sequential commits / one final merge.
11. Party perspective default Claimant/Proactive; localStorage
remembers last-used. URL ?my_side= + ?appeal_filed_by=.
12. Bilateral rules tagged via new is_bilateral column on
deadline_rules; mirroring only when flagged.
Maria's two scope additions folded in:
- Court-system granularity for forum filter (clarification).
- Party-perspective selector absorbing t-paliad-132.
Implementation now starting on this branch.
m's 2026-05-05 brief restructures the page surface that v2 (t-paliad-131)
shipped. The current Fristenrechner stacks three blurred entrypoints —
Phase D search bar, Verfahrensablauf tile grid, "Was kommt nach…" tab.
v3 forks the page so each mental model has its own entry:
- Pathway A — Verfahrensablauf informieren (Browse): existing wizard.
- Pathway B — Frist eintragen aufgrund Ereignis (Event → Deadline),
subdivided into:
- B1 Entscheidungsbaum: data-driven button cascade (CMS-Eingang →
Vom Gericht → Hinweisbeschluss → cards), max 4 deep, back +
breadcrumb + bookmark URLs.
- B2 Filter / Suche: Phase D concept-card search PLUS new
Gericht/System multi-select chip filter (Q8 reversal). All filters
AND-narrow.
Adds two new tables (Phase A — purely additive):
- paliad.event_categories — recursive taxonomy tree, with step
questions on non-leaf nodes.
- paliad.event_category_concepts — leaf → concept junction with
optional proceeding_type_code narrowing.
Existing data layer (deadline_concepts, deadline_rules, trigger_events,
deadline_search matview) untouched. Phase D search handler gains
?event_category_slug= and ?forum= query params; forum-bucket map lives
in Go (UPC / DE LG / DE OLG / DE BGH / DE BPatG / EPA / DPMA).
Phasing: A (data) → B (landing fork) → C (B1 tree) → D (B2 forum
filter) → E (retire legacy tabs, gate-gated). Each phase independently
shippable.
Open questions for m at §10: retire legacy tabs, decision-tree depth,
back/breadcrumb, partial-path bookmarks, multi vs single-select forum,
all-vs-path-matching cards per step, austere icons, 7 forum buckets,
B1↔B2 state-sharing, PR phasing.
Inventor parked. Next: m's go/no-go before coder shift.
Cross-references docs/plans/unified-fristenrechner.md (v2, shipped) for
concept-layer / search-backend / coverage details v3 inherits unchanged.
m's revisions (23:36):
- Q1 corrected: EN slug for shared concepts too (klageerwiderung →
statement-of-defence, replik → reply-to-defence, berufungsfrist →
notice-of-appeal, einspruchsfrist → opposition, wiedereinsetzung →
re-establishment-of-rights). DE slug only for German-law-only
concepts (nichtzulassungsbeschwerde, versaeumnisurteil-einspruch,
hinweisbeschluss-stellungnahme).
- Q4 simplified: drop the customizable-extension flag_param mechanism.
Replace with a generalised "user can override any computed date,
downstream re-anchors off it" capability. CalcOptions gains
AnchorOverrides map[string]string; tree-walk consults it before the
computed-date map. UI gives each row a click-to-edit date affordance
(also unlocks court-set decision dates being entered post-hoc, which
the existing IsCourtSet placeholder UX has been hinting at). PatG §82
seed stays at 2 months static; user-set extensions handled by inline
date override, not by a flag_param mechanism.
Cleaner. No new DB column. Generalises beyond extensions to any case
where the user knows the real date better than the calculator's
projection.
- Q1 concept slug naming: mixed convention. EN slug for UPC/EPC-native
concepts (application-to-amend, request-for-discretionary-review).
DE slug for German-only concepts (nichtzulassungsbeschwerde,
versaeumnisurteil-einspruch). DE slug for SHARED concepts that exist
in both DE and UPC/EPC (klageerwiderung, replik, berufungsfrist,
einspruchsfrist, wiedereinsetzung) because m works primarily in
German and the slug is internal/maintenance-facing only.
- Q2 EU.EPÜ confirmed for EPÜ namespace.
- Q3 PatG §111(1) 1mo→3mo confirmed for Phase B3.
- Q4 PatG §82(1): shape (b) — 1mo base + with_extension flag with
CUSTOMIZABLE extension duration (default 1mo). New flag_param
mechanism on flag-conditioned rules: CalcOptions.Flags becomes
map[string]int; rules with flag_param_code add caller's param to
duration. UI shows number input next to checkbox. Generalises to
PatG §75 etc. Phase A5 picks up the calculator extension; Phase B3
hooks PatG §82.
- Q5 Full Appeal Chain: multiple date inputs per stage, no inter-stage
gap guessing. Stage N's downstream deadlines render as IsCourtSet
placeholders until user enters Stage N-1's terminal decision date.
- Q6/Q7/Q8 confirmed as drafted.
§5.2.2 PatG §82 row updated to reflect flag-based shape. §4.4 concept
slug examples expanded with the mixed-convention rule rendered
explicitly. §7 Phase A5 added for the flag_param calculator change.
Significant restructure after m's 10 answers (relayed via head 23:10):
- Augment, not replace — search bar at top + existing tile grid stays as
browse fallback. Both existing tabs stay live. Phase E (subsumption)
dropped.
- Unifier shape: new paliad.deadline_concepts layer above existing
deadline_rules; deadline_rules gains concept_id FK + structured
legal_source. condition_flag scalar→array (Q3) for AND-of-flags
semantics on UPC_REV (with_amend ∥ with_cci).
- Search hits as ONE card per concept with proceeding pills inside (NOT
a flat list of one-per-proceeding hits). Card body: pills [UPC R.23.1
3mo] [LG §276.1 6w] [BPatG §82.1 1mo] [EPA R.79.1 4mo] etc.
- Structured legal_source codes: UPC.RoP.23.1, DE.ZPO.276.1,
EU.EPÜ.108, DE.PatG.111.1 — parseable, filterable, indexed.
- "Vollständige Instanzenkette" checkbox synthesises LG→OLG→BGH (or
BPatG→BGH) timeline as one tree at render-time; data stays per-
instance.
- Forum filter dropped (Q8). Filters now: Verfahrensart / Partei /
Rechtsquelle.
- Court-set placeholders ("Verhandlung", "Entscheidung",
"Zwischenverfügung") surface as searchable triggers (Q10).
- Columns-view sequence preservation (Q9) flagged but punted to a
separate follow-up task — t-paliad-129 column renderer must respect
sequence_order even on undated court-set events.
8 remaining open questions for m (concept slug convention, EPÜ
namespace, PatG §82(1) modeling, Full Appeal Chain anchor handoff,
quick-pick chip seed, etc.).
Design doc only — no code touched. Recommends keeping /deadlines + /appointments
URLs but rendering one EventsPage component (smallest-diff Option A1), backed
by a new EventService that delegates to existing Deadline/AppointmentService
(Option B1). Two-rail bucket summary on Beides (5 deadline + 3 appointment),
detail pages stay separate, /agenda timeline left alone. §F lists 17 questions
gating m's greenlight, including a premise correction: the brief described
/agenda as the appointment list — actually it's a pre-existing cross-type
timeline; the appointment list is /appointments.
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed
m/patholo → mAi/paliad → m/paliad, but go.mod still declared
`mgit.msbls.de/m/patholo` and every internal import echoed the
pre-rebrand name.
Sweep:
- go.mod: module path → mgit.msbls.de/m/paliad
- All *.go files: imports rewritten via sed
- README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad
- Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in
i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx,
global.css
Verified: go build/vet/test ./... clean, bun run build clean,
no remaining mgit.msbls.de/m/patholo or mAi/paliad references
outside docs that intentionally describe the rename history.
m greenlit all 7 open questions on 2026-04-30 12:23. Notable changes
from the initial draft:
- Submissions are explicitly the primary Event-Type use case, not a
secondary discriminator. m: "those are the event types I mean,
mainly". Deferring a separate paliad.submissions table stands.
- /deadlines + /agenda Typ filter is MULTI-SELECT (UNION across
selected types, AND-intersected with Status/Projekt). New
EventTypeMultiSelect component spec'd in §4: trigger button styled
like the existing <select>s, popover with search + grouped checkbox
list. Status/Projekt stay single-select.
- Firm-wide Event-Type creation OPEN to any authenticated user. RLS
insert policy simplified to created_by=self. Admins moderate via
archive. Mitigation: duplicate-warning in the add modal. Follow-up
t-paliad-089 flagged for admin moderation panel.
- Broader-scope seeds confirmed (UPC + EPO + DPMA + DE + contract).
- §12 rewritten as a resolution table.
Standalone paliad.event_types table with nullable FK on paliad.deadlines,
seeded from a curated subset of paliad.trigger_events (UPC submissions +
decisions) plus hand-written EPO/DPMA/DE-national/contract entries.
Picker on /deadlines/new + edit modal with grouped options + inline
custom-add modal (private types for any user, firm-wide gated to
global_admin). Filter <select> on /deadlines (matching existing
Status/Projekt pattern, not pills) and pill-row on /agenda. Submissions
are NOT a separate entity — category='submission' on event_types carries
the discrimination until a real Schriftsatz-Verwaltung is built.
Awaiting m's go/no-go on §12 before any implementation.
Read-only research deliverable. Compares paliad's 9-proceeding-type
Fristenrechner ruleset (52 public rules in deadline_rules) against
youpc's 70-deadline event-driven calc (data.deadlines + data.events).
Top findings (§1 executive summary):
- youpc covers 64 distinct UPC RoP rule codes; paliad covers ~5
- The two tools answer different questions (timeline-by-procedure vs
search-by-trigger-event) — biggest gap is structural, not data
- Paliad's holiday system is materially better; youpc's defaults are empty
Critical bugs surfaced (§4):
- Public UPC_INF Fristenrechner ignores CCR-conditional rejoinder
duration (always uses 029.c/1mo, should be 029.d/2mo when CCR filed).
KanzlAI internal INF type already wires this; public type doesn't.
- UPC_APP grounds chained off notice instead of decision date,
giving wrong dates when notice is filed early
- EP_GRANT publish chained off filing instead of priority date
- Rule_code format inconsistent across migrations (RoP 23 vs RoP.023)
Recommendations ranked across 5 tiers (§6) for m to review.
Open product decisions in §7. No code changes.
Bundle of small audit findings, all doc-only or dead-code:
- F-5: refresh stale escalation-contact comment in models.User —
Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
- F-10: add "OBSOLETED by migration 018" note to migrations 004/005/006
so readers stop hunting for the live shape in obsolete files.
- F-11: document the data-loss semantics of dropping
paliad.partner_unit_events on the 027 down — audit rows are
append-only telemetry, accepted loss on rollback.
- F-15: drop the patholo_session / patholo_refresh cookie fallback
added during the 2026-04-16 rebrand. Active users have long since
been re-authed through the upgrade path; inactive users hit the
normal /login flow.
- F-16: refresh stale /api/departments comment in team_pages.go to
/api/partner-units (renamed in t-paliad-070).
- F-17: move internal/db/migrations/_dev/mock_supabase_auth.sql to
internal/db/devtools/ so a future loosening of the //go:embed
pattern can't accidentally ship the dev-only fixture.
- F-18: update docs/project-status.md "Audit polish-2" entry — the
batch shipped via t-paliad-067 / 068 / 073, follow-ups are now
tracked under the 2026-04-30 re-audit + t-paliad-074.
go build / vet / test clean.
Read-only audit after the 9-merge push of t-paliad-066..073. Surfaces
18 findings across 7 lenses (service boundaries, naming, frontend↔
backend contract, migrations, tests, dead code, doc drift) plus three
architecture observations carried forward from the 2026-04-18 audit.
Top 3 punch list:
- F-1 (🔴 active): AdminDeleteUser SQL writes to dropped tables
paliad.department_members / paliad.departments. Live production bug,
blocks admin user-delete. user_service.go:768,773. Missed by
t-paliad-070 rename sweep (last touched 2026-04-27, predates rename).
- F-13 (🔴 active): 7 live-DB integration tests skip silently when
TEST_DATABASE_URL unset, no CI exists. Same pattern that masked the
t-paliad-069 reminder bug for ~24h and that hid F-1 above.
- F-2 (🔴 active): visibility predicate inlined in 10 hot-path SQL
sites despite central helper in visibility.go (dashboard/agenda/
reminder/team/deadline service). Inlined sites silently skip the
global_admin shortcut.
No code changes — head sequences dispatch.
m's 21:44 answers expanded the rename scope and resolved all 5 open Qs:
- Naming: partner_unit everywhere (not 'department')
- API + URL rename too: paliad.departments → paliad.partner_units,
/api/partner-units, /admin/partner-units
- Settings admin section: removed
- Audit emit: in this PR (paliad.partner_unit_events table)
- users.dezernat: dropped entirely (not renamed)
Migration 026 now does: best-effort second seed of department_members from
dezernat free-text → DROP COLUMN → rename departments + department_members
tables to partner_units + partner_unit_members → rename junction column to
partner_unit_id → rename constraints/indexes/policies → create
partner_unit_events audit table with RLS.
Single tx, exception-trapped renames for idempotency on freshly-provisioned
DBs.
Onboarding form: free-text input replaced with a partner-unit <select> that
inserts a membership row in the user-create tx. Settings profile loses the
free-text field.
PR strategy: still single PR, ~2200 lines net (heavier than v1 due to
structured-side rename + audit plumbing).
DB-backed templates with embedded fallback, per-language split, full
edit/preview/version/restore loop. Subject moves from Go-built strings to
template-rendered. Five open questions for m parked at §8 — most loaded:
should base.html be editable or read-only.
Inventor design doc for the Dezernate→Partner Units rename and the new
/admin/departments management surface that replaces the placeholder card.
Key proposals:
- Single PR, single migration (026: users.dezernat → users.department).
- New /admin/departments page mirrors /admin/team aesthetic; lifts the CRUD
out of /settings?tab=dezernat.
- User-facing label "Partner unit" / "Partner units" (same in DE+EN per m).
- Defer audit event emission to t-paliad-071 to keep this PR focused.
- Phase 2 follow-up: drop the free-text users.department duplicate once
onboarding can pick from the structured registry.
Five open questions for m in §12 before coder shift starts.
Exposes paliad.users.escalation_contact_id (added in migration 025) via
the Benachrichtigungen tab so users can route DRINGEND/overdue
escalation to a specific colleague instead of the global_admins
fallback.
Service:
- UpdateProfileInput.EscalationContactID *string (empty = clear, matches
Dezernat tri-state pattern). Server-side validation rejects self-
pointer (also enforced by CHECK in migration 025) and unknown UUIDs.
Reminder read path:
- digestRow now carries owner.escalation_contact_id and the audience
predicate adds the override. visibleForCategory's "global admin"
branch suppresses when an override is set, so escalation does not
fan out to the whole admin team. Test table extended with override
cases (escalation contact sees overdue / DRINGEND, admin suppressed).
UI / client:
- New "Eskalations-Kontakt" section under Benachrichtigungen with a
select populated from /api/users (excluding self, sorted by name).
First option is the default-fallback marker; selecting it clears.
- savePrefs PATCHes escalation_contact_id alongside the existing
reminder fields.
i18n: einstellungen.prefs.escalation.{heading,hint,default_option}
in DE + EN.
docs/project-status.md: flips the open follow-up to "shipped".
CLAUDE.md should be AI guidance only. Phase status, shipped milestones,
open follow-ups, and the patHoLo→Paliad rebrand history are project
state — they belong in docs/, not in agent instructions.
Created docs/project-status.md with the full block. CLAUDE.md now points
to it.
Design for zero-overdue SLO, per-user bundled digests (one email per slot
per local-day), DRINGEND evening escalation, and global-admin escalation
on overdues. Includes the actual TZ root cause (alpine container has no
tzdata; LoadLocation silently falls back to UTC) and the embed-tzdata fix.
Awaiting m's go/no-go before implementation.
Survey-only pass across the authenticated paliad surface as test admin
on Playwright at 1280×900 + 375 mobile spot-checks + DE/EN toggle.
Top 10 (best value-per-effort):
1. Strip "Hogan Lovells"/"HL" from public surface (landing, downloads)
2. Pick lime as the single primary green; retire forest-green
3. "Projekt archivieren" red → neutral (reversible, not destructive)
4. /admin/team search input has overlapping placeholder text (visible bug)
5. fristen.field.project.choose raw i18n key on /deadlines/new
6. Activity log leaks project_type_changed + "Type case → litigation"
7. lang="de" on date and time inputs (mm/dd/yyyy + 09:00 AM in DE UI)
8. "Akte" → "Projekt" residue on /deadlines + /appointments
9. Office values lowercased no-umlaut on /projects/{id}/team
10. Project tabs use href="#" — middle-click broken
Plus 40 other findings ranked by severity (broken/friction/polish) and
effort (≤30min/1-2h/half-day+). Suggested 5-PR batching.
41 screenshots in tests/screenshots-polish-2026-04-27/ covering every
sidebar entry + project detail tabs + DE/EN + mobile.
No code changes. Implementation tasks dispatched separately by head.
Conflation: paliad.users.role was simultaneously job title (display only)
and global permission ('role=admin' checks across Go/SQL/JS). m wanted
to set his real job title ('Counsel Knowledge Lawyer') without losing
admin access — the t-paliad-050 admin-team UI even rejected role='admin'
on edit, so any UI-driven update silently demoted m.
Per m's three-axis principle ("firm roles are not project roles are not
tool roles"), this lands TWO orthogonal columns:
* paliad.users.job_title — free text, NULL allowed, display only.
NEVER gates anything in code or SQL.
* paliad.users.global_role — CHECK ('standard'|'global_admin'),
default 'standard'. The only thing that gates ops.
Migration 023:
* Drops NOT NULL + 'associate' default off the legacy role column
* Promotes role='admin' rows to global_role='global_admin'; clears
their role text; sets m's job_title='Counsel Knowledge Lawyer'
* Renames role -> job_title with CHECK (job_title IS NULL OR <> '')
* Replaces can_see_project body with global_role='global_admin'
* CASCADE-rebuilds every RLS policy under canonical English names —
with the historic u.role IN ('partner','admin') gates simplified
to u.global_role='global_admin' only (job_title NEVER gates)
Code surface:
* internal/models/models.go: User.Role -> User.JobTitle (*string) +
User.GlobalRole (string)
* internal/services/user_service.go: bootstrap (first row promoted to
global_admin via pg_advisory_xact_lock(7346298141), unchanged constant);
UpdateProfile drops role, accepts job_title only; AdminUpdateUser adds
global_role with last-admin demotion guard (ErrLastGlobalAdmin);
IsAdmin reads global_role
* Other services (dashboard/agenda/appointment/project/deadline/
department/party/note/checklist_instance): pass user.GlobalRole into
visibility predicates; partner-or-admin gates simplified to
global_admin only
* Handlers: drop now-impossible ErrAdminBootstrapOnly cases;
admin_users handles ErrLastGlobalAdmin -> 409
* department_service: SQL u.role -> u.job_title, DepartmentMember.Role
-> JobTitle (*string)
Frontend:
* /api/me + Me interfaces ship {job_title, global_role}
* Onboarding form: 'Berufsbezeichnung / Job title' (job_title)
* Settings + admin-team forms: same renames + i18n updates
* Admin-team: new 'Berechtigung / Permission' column with
'Standard'|'Global Admin' badge + dropdown editor; last-admin
demotion guard at the UI layer
* Sidebar admin-section reveal: me.global_role==='global_admin'
* deadlines/deadlines-detail/projects-detail/notes: partner-as-permission
gates dropped, only global_admin grants those operations
Tests:
* user_service_test: bootstrap promotes first user to global_admin,
subsequent default to standard; AdminUpdateUser refuses to demote
the last global_admin; IsAdmin reads global_role
Migration applied to ydb 2026-04-27. Live state verified:
* m: job_title='Counsel Knowledge Lawyer', global_role='global_admin'
* tester: job_title=NULL, global_role='global_admin'
* 29 stub colleagues: job_title='associate', global_role='standard'
Conflation today: paliad.users.role is simultaneously job title (display only),
global permission (`role='admin'` checks across Go/SQL/JS), and not-quite-but-
sort-of project_teams.role (already separated). m wants to record his real job
title ("Counsel Knowledge Lawyer") without losing admin access — the existing
admin-team UI even rejects role='admin' on edit, so any UI-driven update
silently demotes him.
Design proposes:
- Rename paliad.users.role -> paliad.users.job_title (free text, NULL allowed)
- Add paliad.users.global_role (CHECK IN ('standard','global_admin'),
default 'standard')
- Single migration 023 does the rename, populates global_role from the old
role, fixes m to job_title='Counsel Knowledge Lawyer', updates
can_see_project, rebuilds RLS policies
- Inventory of every role='admin' call site across services/handlers/
migrations/frontend bucketed by what migrates vs. what stays
- Keeps the existing 'partner' gate as job_title-driven (already broken in
prod — "Partner" capital-P vs lowercase 'partner' check; documented as
out-of-scope follow-up)
- Bootstrap rule (first user becomes admin) keeps the same advisory lock,
flips global_role instead of role
- API surface: /api/me returns both fields; admin-team UI gets a Permission
column with a global_role dropdown + last-admin demotion guard
Awaiting m greenlight before implementation phase.
Design only — no code changes. Five-slot bottom bar for phones (<768px),
center slot opens slide-up Quick-Add sheet (Frist / Termin / Projekt),
right slot reuses the existing mobile sidebar drawer. Tablets and
desktop unchanged. Awaiting m's review before implementation.
Comprehensive design doc for the replacement of flat paliad.akten with:
- paliad.mandanten (Clients as first-class table)
- paliad.projekte (single self-referential typed tree, ltree materialised
path, 5 project types: mandat/litigation/patent/verfahren/projekt)
- paliad.teams + paliad.team_mitglieder (Dezernate + project teams in one
table with kind-shape CHECK)
- paliad.projekt_mitglieder (hot-path junction replacing akten.collaborators)
Polymorphic FK strategy: single project_id FK on fristen/termine/dokumente/
parteien/akten_events/checklist_instances. Notizen keeps its 4-way polymorphic
shape (akte_id renamed to project_id).
Visibility model: tree-connected — seeing any node grants access to the whole
tree (ancestors + descendants). Office-scope stays at project level; Mandant-
level firm_wide_visible / collaborators override.
Migration plan: 6 phases, non-destructive. UUIDs preserved between akten and
projekte rows so child tables only need column renames, no data moves.
Opinionated: German naming throughout (mandanten, projekte, teams,
team_mitglieder, projekt_mitglieder); /akten URLs alias to /projekte
indefinitely; akten_events table name kept for continuity.
Deliverable: docs/design-data-model-v2.md (920 lines, 14 sections).