Two slices on mai/noether/collapse-regel-typ-on:
0c12644 feat(deadline-rules): expose concept's canonical event_type per rule
1e97ecc feat(deadlines/new): auto-link Typ to Regel's concept
What ships:
- New junction paliad.deadline_concept_event_types maps every
paliad.deadline_concepts row to its canonical paliad.event_types
row(s). Many-to-many for concepts with multiple legitimate variants
(statement-of-defence ↔ base + with_ccr + no_ccr; opposition across
EPO + DPMA). Exactly one row per concept marked is_default = true
by a partial unique index — that is the row the deadline form
auto-fills with.
- Backend: paliad.deadline_rules_with_concept_event_type view + the
deadline-rules read path now expose the rule's default concept
event_type so the form has the auto-fill target without an extra
round-trip.
- Frontend deadline create / edit form: when the user picks a Regel,
the Typ chip auto-fills with the rule's concept's default event_type.
A small "vorgegeben durch Regel — überschreiben?" hint sits next to
the chip so the auto-fill is visible. The user can override (free-
text or pick a different type); the override is explicit, no
blocking validation.
- Free-text Typ stays available — manual deadlines without a
matching rule (e.g. "Call me" reminders) keep working as today.
Migration housekeeping
======================
noether authored her migration as 072 on her branch but main had
already taken 072 via minkowski's t-paliad-164 (paliad.projects.our_side).
Renumbered to 073 during merge resolution to resolve the same-number
collision. Added IF NOT EXISTS guards on CREATE TABLE / CREATE INDEX
for re-run safety (the seed INSERT already had ON CONFLICT DO NOTHING).
Live tracker bumped 72 → 73 in the same operation: both effects
(our_side column AND deadline_concept_event_types table) were
applied to live during dev (each worker against the same DB), so
the tracker advance reflects schema reality. Next deploy sees
tracker=73 with file 073 present and has nothing to apply.
Refs m/paliad#18.
Three slices on mai/minkowski/project-level-our-side:
188d8ec Slice 1 — paliad.projects.our_side column + service plumbing
5d9c62d Slice 2 — "Wir vertreten" select on the project edit form
3a41ace Slice 3 — Determinator predefines perspective from our_side
What ships:
- Migration 072 adds paliad.projects.our_side text with check constraint
IN ('claimant','defendant','court','both', NULL). Idempotent
(IF NOT EXISTS / DO blocks). NULL stays the default.
- Project model + service plumbing: OurSide *string on models.Project,
threaded into Create / Update / SELECT projections + handlers.
- Project edit form: new "Wir vertreten" select with the four options
+ "unbekannt / nicht gesetzt", DE+EN i18n.
- Fristenrechner Determinator (Slice 3c — perspective chip): when a
project is selected and our_side is set, the chip is predefined to
that value with a "vorgegeben durch Akte" hint above. The user can
still override (chip click); the override is explicit. When
our_side is NULL, the existing free-pick behaviour stays.
m's dogfood (2026-05-08 21:42): "We chose a case of ours where our
side should be predefined - yet I can make a selection for which
side we are." Now resolved end-to-end: edit the project once to set
"Wir vertreten = Klägerseite", and the Determinator perspective chip
auto-locks to that side on every subsequent visit.
When the user picks a Regel on /projects/{id}/deadlines/new (or the
global /deadlines/new), auto-populate the Typ chip with the rule's
concept's canonical event_type — using the
concept_default_event_type_id field server-side hydrated by mig 072.
Soft hint "Typ vorgegeben durch Regel — entfernen, um zu überschreiben"
when the chip exactly matches the rule's suggestion. Soft warning
"Hinweis: Typ widerspricht Regel" when the user has picked an event_type
that contradicts the rule's concept.
The picker is replaced silently when it still reflects the previous
rule's auto-fill (or is empty); leaves a manually-edited picker alone.
DE+EN i18n via deadlines.field.rule.{autofill,mismatch}. Reuses the
existing .form-hint--warning yellow-tint style; no new CSS.
Closesm/paliad#18 Item A — rule-vs-event redundancy on the manual
deadline create form.
Closes m's 2026-05-08 21:42 dogfood loop: when the user picks an Akte
that knows its own side, the Determinator perspective chip should be
locked to that side instead of asking the user to re-pick something
the project already knows.
ProjectOption gains our_side; the JSON already carries it from
slice 1 (ProjectService.projectColumns). New helper
applyOurSidePredefine maps project.our_side onto the chip:
claimant → "claimant" chip active
defendant → "defendant" chip active
court → null chip cleared (court actions are neutral
to the user's side, so no narrowing)
both → null explicit "Beide" intent
null/undef → no-op
URL wins: if ?role= is present at call time the user (or a shared
link) chose it explicitly and we don't overwrite. When we do predefine,
we write the same value to the URL so refresh + back/forward round-trip
correctly. Two call sites:
- selectProject: in-page Akte pick. push history (replaceURL=false) so
back-button restores the prior state.
- post-fetchProjects hydration: the deep-link / refresh path. Use
history replace so the URL stays clean.
A small "vorgegeben durch Akte" / "predefined from project" hint
renders next to the chip strip (italic muted). Visible whenever the
active perspective came from the project; cleared on any chip click
(explicit override) and on Step-1 reselect (no Akte = no hint).
popstate restores hint visibility by recomputing from
project.our_side ↔ currentPerspective so back/forward feels right.
Free-pick is preserved: clicking another chip overrides the
predefine and the cascade re-narrows immediately.
Add paliad.deadline_concept_event_types junction (mig 072) mapping each
deadline_concept to its canonical paliad.event_types row(s). Hydrate
DeadlineRule.ConceptDefaultEventTypeID via one IN query per List call so
/api/deadline-rules carries the autofill hint for the deadline create
form (t-paliad-165 / m/paliad#18).
Seed mapping covers the active concepts driving existing rules — 29
rows across 26 distinct concepts. Concepts without an obvious event_type
counterpart (decision, filing, grant, the DE-only Begründung family)
stay unmapped; auto-fill silently skips them.
ProjectFormFields gains a fifth select between case-specific block and
the description textarea: "Wir vertreten" with options claimant /
defendant / court / both / "" (the unset sentinel labelled
"Unbekannt / nicht gesetzt"). Type-agnostic — every project type
carries it because the Determinator picks it up regardless. Form-hint
explains it predefines the Determinator perspective and stays
overridable.
client/project-form.ts: readPayload writes our_side as a normal
stringField (empty string in edit mode clears the column via the
nullableOurSide helper on the service); prefillForm hydrates the
select from p.our_side. Both gate on tryGet so /projects/new (which
shares the form) still loads if the field is later removed.
i18n already in slice 1; this commit only wires the markup +
client logic.
m's 2026-05-08 21:42 dogfood feedback on the Determinator perspective
chip: when an Akte is selected, the chip should be locked to the firm's
known side instead of asking the user to re-pick. paliad didn't track
that anywhere — paliad.parties.role records each party's role but no
flag for "this is the side we represent".
Migration 072 adds paliad.projects.our_side text with a CHECK
constraint (claimant | defendant | court | both | NULL). NULL stays the
default so existing rows are neutral and the Determinator falls back to
free-pick. Idempotent (ADD COLUMN IF NOT EXISTS + DO-block guarded
constraint) so a re-run against a partially-applied state is safe —
paliad has been bitten by collision twice this week.
Project model + ProjectService:
- OurSide *string field on models.Project
- CreateProjectInput / UpdateProjectInput accept our_side
- INSERT and partial UPDATE thread the value through; validateOurSide
rejects unknown enum values with ErrInvalidInput before the DB
constraint would; nullableOurSide turns "" into NULL so the form's
"unset" sentinel can clear the column
- Update logs an our_side_changed audit event with "<from> → <to>"
description (matching status_changed / project_type_changed
shape); both ends use the literal "none" sentinel for NULL so the
frontend renderer can map it to projects.field.our_side.none
i18n: event.title.our_side_changed (DE/EN), dashboard.action.short
verb form, projects.field.our_side.{label,hint,unset,claimant,
defendant,court,both,none} for the upcoming Slice 2 select.
Frontend translateEventDescription gets an our_side_changed branch
that runs translateArrowSlugs over the projects.field.our_side.*
prefix so the Verlauf tab renders localized labels.
Slice 2 wires the form, Slice 3 wires the Determinator.
Two follow-up slices on the inline-Paliadin scope (m's 2026-05-08 21:37):
1782dfa Slice F — cross-surface DB-driven history hydrate
ae1cba4 Slice G — tmux crash-recovery primer
What ships:
- The inline drawer (client/paliadin-widget.ts) and the standalone
/paliadin page (client/paliadin.ts) now share one session id and
one history bucket. localStorage stays as a render-cache only;
the DB (paliad.paliadin_turns) is source of truth. Both surfaces
hydrate from GET /api/paliadin/history?session=<id>&limit=N on
mount, then reconcile localStorage with the server response (always
prefer server). Eliminates the trap klaus warned about (paliad#19,
the localStorage short-circuit that hid late server-side responses).
- A turn typed into the drawer now shows up when the user opens
/paliadin and vice versa, on both the same browser and across
refreshes.
- tmux crash-recovery primer: when LocalPaliadinService /
RemotePaliadinService detects a fresh pane (tmux session label
rotated, or no prior turn output in the response dir), it injects
a context-dump primer with the last N exchanges from
paliad.paliadin_turns BEFORE the new prompt lands. The persona
catches up on the conversation rather than starting from zero.
Primer format documented in scripts/skills/paliadin/SKILL.md.
Auth gate unchanged: /api/paliadin/history honours PaliadinOwnerEmail
just like /api/paliadin/turn. Tests added for the hydrate + reconcile
+ primer paths in paliadin_test.go.
When a user's tmux session dies (mRiver reboot, OOM, manual kill,
container restart) the next turn used to wake claude with NO prior
context — the persona had to derive everything from the new turn
alone. Now: when the Go side detects a fresh pane, it pulls the last
N exchanges from paliad.paliadin_turns and prepends them as a
[primer …][/primer] block to the next user envelope.
Format SKILL.md parses (single-line, control-chars stripped):
[PALIADIN:<turn_id>] [primer last=N] U: … \n A: … \n … [/primer] [ctx …] <Frage>
Detection paths:
- Local (LocalPaliadinService): ensurePane now returns
(target, isFresh, err). isFresh is true when no prior
@paliadin-scope=chat window existed and we created one. RunTurn
passes that into buildPrimerIfFresh.
- Remote (RemotePaliadinService): can't see across the SSH boundary
to know the pane's true freshness, so we approximate with a
per-(session, Go-process) "primed" cache. First turn after
process-start, ResetSession, or healthGate failure rebuilds the
primer; subsequent turns skip it. ResetSession + healthGate failure
both call clearPrimed(session) explicitly.
paliadinDB.buildPrimerIfFresh assembles the block:
- Reads the last MaxPrimerTurns=5 exchanges from
ListHistoryForSession (Slice F).
- truncateForPrimer normalises each side (drops \r\n, collapses
whitespace, caps at MaxPrimerCharsPerSide=600 with …).
- Returns "" silently when isFresh=false, no SessionID, no prior
history, or DB error — the user's actual question still lands; we
only lose the recap.
SKILL.md (~/.claude/skills/paliadin/SKILL.md, refreshed via
scripts/install-paliadin-skill) gets a new "Crash-recovery primer"
section above the context-envelope block. Five behaviour rules:
1. Don't re-execute prior tool calls (audit log already has them).
2. Use the primer for thread continuity, not as a data source.
Re-call tools for fresh facts.
3. Truncated lines (ending in …) are partial — paraphrase rather
than quote.
4. No primer at all = normal case (existing pane, history is in
tmux memory). Behave as before.
5. Acknowledge sparingly — usually just answer the actual question
with the recap as silent context.
New test TestTruncateForPrimer pins the per-side truncation contract
(no \r\n leaks, repeated spaces collapsed, ellipsis on oversized
input, short input untouched). go test green.
Refs: docs/design-paliadin-inline-2026-05-08.md §6
(deferred Anthropic API cutover prereq).
Two Paliadin chat surfaces shared a user but not their conversation:
the inline drawer (paliadin-widget.ts) maintained `paliadin:widget:session`
+ `paliadin:widget:history:` while the standalone /paliadin page used
`paliadin:session` + `paliadin:history:`. A turn typed in the drawer
never surfaced on /paliadin and vice versa, and a localStorage wipe
tossed everything.
Fix in three coordinated parts:
1. **Shared session id.** The widget now uses the same `paliadin:session`
key the standalone page already uses. One-time migration in
bootSession copies any legacy `paliadin:widget:session` across so
existing users keep their conversation thread, then deletes the legacy
key. The widget's HISTORY_PREFIX also drops the `widget:` namespace
so both surfaces' render-caches address the same bucket.
2. **DB-driven history.** New endpoint:
GET /api/paliadin/history?session=<id>&limit=<N>
Returns the caller's turns for the session, oldest → newest,
gated by PaliadinOwnerEmail (same gate as POST /api/paliadin/turn).
Backed by paliadinDB.ListHistoryForSession, which mirrors the
existing visibility predicate (own rows always; all rows for
global_admin). Default limit 50, capped at 200.
3. **Hydrate-on-mount, hydrate-on-open.**
- paliadin.ts (standalone page): DOMContentLoaded calls
hydrateFromServer() right after renderHistory() seeds from
localStorage. DB rows replace the cache when present.
- paliadin-widget.ts (inline drawer): revealIfOwner kicks
hydrateFromServer in the background after rehydrateHistory paints
the cache. openDrawer() also calls hydrateFromServer so a turn the
user typed on /paliadin since the last drawer-open shows up
without a manual reload.
Reconciliation: DB > localStorage when DB has rows. DB call fails or
returns empty → keep showing whatever's in cache (offline cushion).
This kills the trap klaus warned about (paliad#19): every render
reconciles against the server, no first-paint short-circuits.
Schema: zero migrations. paliad.paliadin_turns already carries
session_id + user_message + response + ts since the t-paliad-146 PoC;
this slice just adds a typed read path.
Backwards compatible: the standalone /paliadin page's session key is
unchanged; only the widget migrates onto it.
Builds + tests green; i18n unchanged.
Refs: m/paliad#19 (localStorage short-circuit), m/paliad#20 (inline modal),
docs/design-paliadin-inline-2026-05-08.md §3.4.
m's 2026-05-08 21:28: "The Projektbaum inside a Project in the tab
with the Unterordner should just be the same as the Tree in Projects.
It has symbols, everything. That should be a shared component."
Drop the inline mini-tree renderer (renderTreeNode / loadProjectTree /
~50 lines of duplicate logic) in client/projects-detail.ts and mount
the existing client/project-tree.ts module into the tab's container.
The shared component carries:
- per-type icons (Mandant / Litigation / Patent / Case)
- pin star (touch-friendly)
- overdue / open-deadline badges with subtree counts
- status chip + type chip
- expand / collapse toggles
- inherited-visibility marking
- search highlighting (no-op when no search params are passed)
Current project highlight: set aria-current="true" on the matching
.projekt-tree-node after mount. The shared CSS already styles
.projekt-tree-node[aria-current="true"] > .projekt-tree-row with the
lime accent (global.css :5853).
Removed the now-dead mini-tree CSS block that was also accidentally
overriding .projekt-tree-title from the real tree (later-defined rule
won the cascade and erased the shared title weight).
loadChildren() still fetches /api/projects/<id>/children for the
empty-state gate ("Keine untergeordneten Projekte" when this node has
no direct children) and the create-link parent_id pre-fill — both
predicates depend on direct children, not the visible tree.
m's dogfood 2026-05-08 21:16: project card for "UPC-CoA Berufung Huawei"
showed "4 offen" but "Nächste Termine — keine bevorstehenden Termine"
even though the four pending deadlines exist with future due dates.
Live container log:
ERROR service: cards preview appointments:
pq: column t.starts_at does not exist at position 13:41 (42703)
The cards-preview appointments query used `t.starts_at`; the actual
column on paliad.appointments is `start_at` (singular). The query
errored, CardsPreview returned (nil, error), the handler returned a
500, and the frontend's `r.ok ? r.json() : []` fell through to an
empty preview map for every project — so deadlines that the deadline
half of the same function had already loaded never reached the card.
"4 offen" stayed visible because that count comes from BuildTreeWith-
Options, a separate query untouched by the bug.
Fix: rename starts_at → start_at in the rowAppointment db tag, the
ORDER BY, the WHERE clause, and the SELECT projection. StartsAt as
the Go field name stays — only the db tag + SQL identifiers change.
Same column name everywhere else in the codebase already used start_at.
m's 2026-05-08 21:11: the changelog entry was sharing the sparkle ✨
glyph with the new Paliadin AI surface (inline widget trigger, agent-
suggested provenance pill, /paliadin entry). Now that ✨ carries an
explicit AI semantic in paliad's visual language, swapping the
changelog to a newspaper SVG keeps the two affordances orthogonal.
m's dogfood 2026-05-08 20:35: a deadline showed an approval-pending
banner on its detail page but did not appear in /inbox under either
"Zur Genehmigung" or "Meine Anfragen". Live container log:
ERROR service: list submitted by user: sql: Scan error on column
index 5, name "pre_image": unsupported Scan, storing driver.Value
type <nil> into type *json.RawMessage
Root cause: paliad.approval_requests.pre_image is NULL whenever the
lifecycle_event is 'create' (no prior row state to capture). The Go
ApprovalRequest struct binds it as json.RawMessage, which is a []byte
typedef that does NOT implement sql.Scanner — sqlx fell back to
*json.RawMessage and choked on the NULL. Same hazard on .payload for
'complete' / 'delete' rows where there's no payload either.
The handler returned the resulting error as a 500, the inbox.ts catch
swallowed it as a network failure, and rendered the empty state. Both
tabs were dark because both list paths hit the same scan.
Fix: introduce models.NullableJSON, a []byte typedef that implements
sql.Scanner / driver.Valuer / json.Marshaler / json.Unmarshaler and
treats NULL ↔ nil cleanly. Inline JSON output is preserved (no base64
cast that bare []byte would have caused). Bind PreImage + Payload to
NullableJSON; existing call-sites (approval_service.go:606) keep
working — both json.RawMessage and NullableJSON are []byte under the
hood, and len() / json.Unmarshal accept either.
Other nullable jsonb columns (User.EmailPreferences, *Metadata) are
all NOT NULL with default '{}' so they don't hit the same path; left
as json.RawMessage.
Verified: live tracker is at v71, no schema change needed; approval
service tests green; /api/inbox/mine query against prod returns the
three expected rows for m once the binary picks this up.
When Claude writes the response file after the 60 s pollForResponse
window expires (e.g. the tmux pane was busy mid-turn when the message
arrived), the SSE stream has already closed with an error and the
file sits unread on disk forever. The chat shows a permanent timeout
even though the answer exists.
Backend:
- LocalPaliadinService.StartJanitor: scans responseDir every 2 s and
patches rows whose response is still NULL when the file lands.
completeTurnLate stamps error_code='late' so the FE can render a
marker. Guarded with WHERE response IS NULL to never overwrite a
real response if RunTurn races.
- Paliadin.GetTurn(callerID, turnID) on the shared paliadinDB. Same
visibility predicate as ListRecentTurns.
- GET /api/paliadin/turns/{id} — owner-gated; lets the chat UI
discover late-arrived responses without a refresh.
Frontend:
- paliadin-late-poll.ts: shared 3 s / 10 min poller.
- paliadin.ts + paliadin-widget.ts: on SSE error, show
"wartet auf späte Antwort", kick off the poller, swap bubble in
place when response arrives + retroactively persist to history.
- i18n: paliadin.late.waiting + paliadin.late.marker (DE/EN).
- CSS: --late-pending opacity tweak, --late neutral background,
italic-grey "verspätet" tag.
m's dogfood 2026-05-08 20:35: "the paliadin hook does not always work — it
does not confirm the claude / terminal command... like lacking an enter
key. Or too fast."
Race between two consecutive tmux send-keys calls: the first writes the
prompt literally with `-l`; the second sends an Enter key event. Claude
Code's TUI debounces keyboard input. When the Enter lands while the
paste is still being absorbed, the carriage-return collapses into the
input buffer as a literal newline character instead of registering as a
"submit" gesture — the prompt sits typed but unsubmitted, and the
backend's pollForResponse then times out on the missing response file.
Fix: sleep 200ms between the literal paste and the Enter. Below the
human-perceptible threshold but well above tmux's pty flush window and
the TUI's input-debounce window. Applied to both code paths:
- scripts/paliadin-shim:send_to_pane (the SSH/RPC production path)
- internal/services/paliadin.go:LocalPaliadinService.sendToPane
(the laptop-only direct-tmux path)
The Go-side variant uses a context-aware sleep so request cancellation
still propagates correctly.
Production shim copy at /home/m/.local/bin/paliadin-shim refreshed
locally on mRiver so the next turn picks up the fix without waiting
for redeploy. (The Dokploy container does not run paliadin — gate on
PaliadinOwnerEmail is owner-only and prod has no claude+tmux anyway —
so no deploy step required for the shim path.)
m's dogfood findings 2026-05-08 20:32:
1. /views/new: form sections (.form-section fieldsets) had no
container CSS, so content rendered against the bare browser-default
fieldset border with effectively zero padding. Adds proper padding
(1.25rem 1.5rem 1.5rem), 8px radius, surface background, and tidies
the legend / hint typography. Reused by every entity-form fieldset
that adopts .form-section.
2. Sidebar nav: collapses the prior split between the "Ansichten"
group (Fristen + Termine) and the "Meine Sichten" group (user-
defined views + "+ Neue Sicht") into a single "Ansichten" group.
Same DOM hook (#sidebar-views-items, .sidebar-views-new) so
client/sidebar.ts's user-view hydration keeps working unchanged —
the entries just sit alongside the built-ins now instead of in
their own labelled section.
m's dogfood findings on the inline drawer:
1. Assistant responses (markdown headings, bold, lists, [chip:nav:…]
tokens, [#deadline-OPEN:<id>] tokens) showed as raw text. The widget
was setting body.textContent and skipping the renderer. Extracted
the standalone /paliadin page's pipeline into client/paliadin-render.ts
(renderResponseHTML + chip helpers + block markdown parser) so both
surfaces share one source of truth. The widget now feeds assistant
bubbles through innerHTML; user bubbles still go through textContent
(no point parsing the user's typed markup).
2. Floating trigger button rendered the sparkle glyph in white-on-lime
in dark mode — color: var(--color-text) inherits the dark-mode light
foreground and washes out completely on a lime background. Lime is
inherently a light-background colour, so the trigger pins its
foreground to --hlc-midnight in both themes.
Bubble CSS additions: assistant bubbles get white-space: normal (the
base pre-wrap rule was forcing every source newline to a literal break
and breaking <p>/<h2>/<ul> spacing) plus tight h2/h3/p/ul margins so
the rendered markdown reads as a chat bubble, not a doc page.
2f27620 — Slice 3b: B1 cascade narrows by the project's proceeding
type. Three-input priority chain (inbox chip > ad-hoc context >
project's proceeding_type). cachedProceedingTypes lookup via
/api/proceeding-types-db; forumFromProceedingCode maps UPC_/DE_/EPA_/
EP_/DPMA_ → upc/de/epa/dpma. /tools/fristenrechner?project=<uuid>&path=b
auto-narrows the cascade without needing chip clicks.
6fcf34a — Slice 3c: perspective chip (Klägerseite/Beklagtenseite/Beide)
at top of the B1 panel. Mig 071 adds paliad.event_categories.party
text[] (claimant|defendant|both|court) with conservative backfill —
claimant on klage.* + replik-*, defendant on widerklage.* + duplik-*.
Cross-appeal/Anschlussberufung leaves stay NULL pending dogfood
(role flips depending on who appealed first). Cascade hides leaves
whose party tag contradicts the chip; both/court tags always pass;
NULL stays neutral. URL-only state (?role=claimant|defendant).
Live tracker is already at v71 (feynman applied 071 during dev).
Deploy will see tracker=71 with file 071 present — no work to apply.
The full Determinator scaffold is now in place:
Step 1 (Akte/ad-hoc) → Step 2 (do/happened) → Step 3a (File/Draft/Enter)
for outgoing or Pathway B with auto-narrowing for incoming.
Open follow-ups (small, can wait for dogfood):
- Tag cms-eingang.gegenseite.upc-rev/upc-app/de-bgh-* leaves with party
(currently neutral; appellate leaves should arguably be tagged based
on who appealed)
- Persistence for perspective if dogfood says it's wanted
- /drafts route + Step 3a Draft card wiring (proper drafting surface
is a separate workstream)
e824898 — feat(navbar/dashboard): per m's 2026-05-08 20:05 design
decisions:
- Sidebar restructured into named groups: Home → Paliadin (gated to
PaliadinOwnerEmail) → Overview (Projekte) → Views (Fristen,
Termine) → Tools (Fristenrechner et al). Group headers render as
small uppercase muted labels.
- Agenda removed from sidebar + BottomNav. Direct link /agenda still
routes; the dashboard now renders Agenda inline as a section.
- "Letzte Aktivität" relocated to sit under Agenda on the dashboard.
- All dashboard sections become collapsible with a chevron toggle;
open/collapsed state persists per-section in localStorage under
paliad:dashboard:collapse:<section>.
- Agenda rendering primitives extracted into client/agenda-render.ts
so the standalone /agenda page and the dashboard's inline Agenda
share identical rendering with no fork.
Pure frontend change — no Go work, no migrations.
m's 2026-05-08 18:09 spec — Slice 3c. Adds a Klägerseite / Beklagtenseite
chip strip at the top of the B1 cascade panel; cascade leaves tagged
with a contradictory party get hidden. Klägerseite never files
Klageerwiderung; Beklagtenseite never files Klageschrift.
Migration 071 adds `paliad.event_categories.party text[]` (CHECK on
{claimant, defendant, both, court}) plus a partial GIN index. Backfill
is conservative — only the obvious leaves get tagged on this pass:
- claimant ich-moechte-einreichen.klage.* (9 leaves)
ich-moechte-einreichen.spaetere-schriftsaetze.replik-*
- defendant ich-moechte-einreichen.widerklage.*
ich-moechte-einreichen.spaetere-schriftsaetze.duplik-*
cms-eingang.* (incoming) and frist-verpasst.* (anyone misses a
deadline) stay NULL because the user can be on either side and still
receive the same court communication. Cross-appeal / Anschluss-
berufung / Reply-to-cross-appeal also stay NULL — the role flips
depending on who appealed first; the cascade doesn't have that
context yet. Tag in a follow-up once dogfood validates the chip.
Backend: EventCategoryNode JSON gains optional `party` array;
EventCategoryService.Tree SELECT picks it up via pq.StringArray.
Frontend: new Perspective type + URL state (?role=claimant|defendant)
+ perspective chip strip styled identically to the inbox-channel chip
strip. perspectiveAllowsParty(party) gates each cascade child;
"both"/"court" tagged nodes always pass; neutral nodes always pass.
Persistence is URL-only — dogfood will tell us whether to add a saved
default later.
Migration applied to live Supabase; tracker at v71.
Refs t-paliad-157 / m/paliad#15.
Sidebar:
- Paliadin lifted out of Übersicht to a top-level entry directly under
Home (owner-only reveal logic unchanged — same id reused).
- Agenda removed from sidebar; the standalone /agenda route stays for
direct-link compatibility but the dashboard hosts its content inline.
- Projekte moved into Übersicht; Fristen + Termine moved into a new
Ansichten group; the Arbeit group is gone.
- Werkzeuge / Wissen / Ressourcen collapsed into one Werkzeuge group
per m's brief order (calculators → reference → content).
- BottomNav agenda slot repointed to /events?type=deadline so the
overdue+today badge still has a sensible target on mobile.
Dashboard:
- Agenda renders inline as a new collapsible section between the
upcoming-rails grid and Letzte Aktivität, with a "Vollständige Agenda
öffnen →" link to the standalone page.
- Letzte Aktivität moved under Agenda per m's design call.
- Sections (summary, deadlines, appointments, agenda, activity) become
collapsible via a chevron toggle; state persists in
localStorage[paliad:dashboard:collapse:<section>]. Matters card stays
whole-card-tappable, so it's intentionally left non-collapsible.
- Inline agenda fetches /api/agenda directly with a 30-day window and
refreshes on the existing 60s dashboard poll.
Render primitives:
- New client/agenda-render.ts hosts renderAgendaTimeline + AgendaItem
type, shared by client/agenda.ts and client/dashboard.ts. Standalone
agenda.ts shrinks accordingly; behaviour is identical.
i18n:
- Added nav.group.ansichten + dashboard.agenda.* + dashboard.section.*
keys (DE/EN). Removed nav.group.{arbeit,wissen,ressourcen} (no other
callers; i18n-keys.ts auto-regenerated).
m's 2026-05-08 18:09 spec: "if we have the project type defined, we
should only have events available that match the type of project /
type of case." Slice 3b wires the project's proceeding_type into the
cascade narrowing alongside the inbox chip and ad-hoc context.
Three inputs feed the cascade now, in priority order:
1. Inbox chip (cms / bea / posteingang) — user override.
2. Ad-hoc Step 1 chip (upc / de / epa / dpma).
3. Project's proceeding (Step 1 picked Akte → proceeding_type_id →
proceeding_types.code → forum prefix).
activeForumOnPage() returns the first non-null value. The B1
cascade's inboxFilterAllowsForums consults this so a user landing on
/tools/fristenrechner?project=<uuid>&path=b&mode=tree gets the
narrowed cascade automatically — no chip clicks required. The chip
can still override at the top of the panel.
Pieces:
- ProjectOption gains optional proceeding_type_id (already on the
JSON; just declared so TypeScript can read it).
- cachedProceedingTypes Map<int, string> is populated once on init
via /api/proceeding-types-db and cached for the page lifetime.
- forumFromProceedingCode() maps "UPC_INF" / "DE_NULL" / "EPA_OPP"
/ "EP_GRANT" / "DPMA_OPP" → upc / de / epa / dpma. EP_ and EPA_
both hit the EPA branch since EP_GRANT belongs to the EPA forum.
- triggerCascadeRefresh() is called from selectProject /
selectAdhoc / clearStep1Context + after the async load completes
so the cascade re-renders when the context changes.
The role variants (Klägerseite vs Beklagtenseite, Berufungskläger vs
-beklagte) are Slice 3c — they require fetching the user's
project_teams.responsibility for the selected project. Project's
forum lands first; role layers on after.
Refs t-paliad-157 / m/paliad#15. Folds in part of #18 (Item A
rule-vs-event collapse) — when the project context narrows the cascade
to one jurisdiction, the rule-vs-event mismatch surface shrinks.
m's request 2026-05-08 20:12: alongside Paliad's per-recipient
"E-Mail an Auswahl" broadcast (which sends individual envelopes from
the server), users want a one-click way to compose a single multi-
recipient email in their own mail client. Common use case: writing
to a specific team where the response thread should stay client-side
and be visible to every recipient (unlike the privacy-preserving
broadcast where each recipient sees only themselves).
Adds a "Im Mail-Client öffnen" / "Open in mail client" link to the
broadcast modal's recipient summary, alongside the existing
"Alle anzeigen" toggle. Clicking it opens a `mailto:` URL with every
selected recipient comma-separated in the To: line per RFC 6068.
`buildMailtoHref` is exported so it can be unit-tested independently
and reused by other selection surfaces (admin team table, project
team tab) without a refactor.
The existing server-driven broadcast path is unchanged — both options
coexist.
Six commits from mai/dirac/inventor-inline-paliadin (all sliced per
the design's §10 phasing):
142edca docs(paliadin): t-paliad-161 inventor design
282e0bb feat(paliadin/migration-070): Slice A — schema + relay seam
0d1a7ba feat(paliadin/context): Slice B — structured page-context payload
ba2408e feat(paliadin/inline-widget): Slice C — floating button + drawer
a3052eb feat(paliadin/suggest): Slice D — agent-suggested write path
4ecea7a feat(paliadin/agent-glyph): Slice E — ✨ alongside 👀
What ships:
- Floating Paliadin trigger bottom-right + Cmd/Ctrl-K shortcut, opening
a 420px right slide-out drawer (full-screen on mobile). Visible on
every authenticated page except /paliadin, /login, /onboarding.
Same PaliadinOwnerEmail gate as today — no scope expansion.
- Per-route starter-prompt registry in client/paliadin-starters.ts —
context-aware empty-state nudges users into useful first prompts.
- Structured PaliadinContext payload (route_name + primary_entity_type
+ primary_entity_id + user_selection_text + view hints) flowing from
the widget through Go into the tmux envelope. SKILL.md gains [ctx …]
parsing so the persona can use it.
- Agent-suggested write path: paliad__suggest_deadline +
paliad__suggest_appointment + paliad__suggest_note tools that draft
rows straight into the existing approval pipeline. Suggestions land
as approval_requests with requester_kind='agent' and an
agent_turn_id pointer back to the originating turn.
- Visual provenance: ✨ glyph alongside 👀 on pending-approval rows
whose request was agent-drafted; persistent ✨ on approved-from-agent
rows in the audit log. Lives in events.ts/agenda.ts/inbox.ts.
Migration 070 is idempotent (every ALTER guarded by IF NOT EXISTS,
constraints/index inside DO blocks). Live tracker is at v69; deploy
will apply 070 cleanly. Adds:
paliad.approval_requests.requester_kind text + xor-check
paliad.approval_requests.agent_turn_id uuid
paliad.paliadin_turns.context jsonb
m greenlit all 5 inventor decisions (a-a-a-a-a) on 2026-05-08 19:39:
owner-only gate, tmux relay v1, create-only suggestion verbs,
✨-alongside-👀 visual, selection-text default-on.
Refs m/paliad#20, design doc docs/design-paliadin-inline-2026-05-08.md.
When a pending row was drafted by Paliadin (requester_kind='agent' on
its in-flight approval_request), surface a sparkle ✨ next to the
existing eye-pill 👀. The two glyphs are orthogonal: 👀 = "needs
approval", ✨ = "Paliadin drafted this". Either can change without the
other, so the visual taxonomy stays decomposable for any future
autopilot mode where 👀 disappears but ✨ stays.
Read-path:
- DeadlineService.ListVisibleForUser + AppointmentService.ListVisibleForUser
LEFT JOIN paliad.approval_requests on pending_request_id and project
ar.requester_kind into the row. NULL when no request is pending.
- models.DeadlineWithProject + AppointmentWithProject grow
RequesterKind *string. The list-projection helpers
(projectDeadline / projectAppointment in event_service.go) carry it
into EventListItem.
- /api/events response now includes requester_kind on every pending
row; /api/inbox already does (Slice D extended approvalRequestViewColumns).
Render-path:
- frontend/src/client/events.ts — new AGENT_PILL_GLYPH constant ("✨"),
agentPill rendered into the title cell next to the existing
pendingPill when item.approval_status='pending' AND
item.requester_kind='agent'. EventListItem TS shape gains
`requester_kind?: "user" | "agent"`.
- frontend/src/client/agenda.ts — same pattern, agendaItem TS shape
+ agentPill rendered next to pendingPill in the headline span.
- frontend/src/client/inbox.ts — ApprovalRequestView gains
requester_kind + agent_turn_id; the meta line replaces the
requester's plain name with "Anna ✨ Paliadin" when the request was
drafted by the agent.
CSS: new .approval-pill--agent modifier in global.css using only
existing tokens (--color-bg-lime-tint / --color-surface-2 /
--color-text), mirroring the .approval-pill--icon shape so the two
glyphs sit side-by-side at the same baseline.
i18n: 3 new keys × 2 langs (approvals.agent.label /
approvals.agent.byline / approvals.agent.suggestion_pending) — total
1966 → 1969.
Build clean (frontend + go), tests green.
Refs: docs/design-paliadin-inline-2026-05-08.md §8.
Paliadin can now draft deadlines + appointments through two new
owner-gated HTTP endpoints. Drafted entities land in the existing
approval pipeline as approval_status='pending' with
requester_kind='agent' + agent_turn_id linking back to the chat turn
that produced the suggestion. The user reviews via the same eye-pill
👀 surface (with ✨ added in Slice E).
POST /api/paliadin/suggest/deadline
POST /api/paliadin/suggest/appointment
Wiring:
- ApprovalService.SubmitAgentCreate — agent variant of SubmitCreate;
always creates an approval_request (bypassing policy lookup) and
stamps requester_kind='agent' + agent_turn_id. Required-role defaults
to 'associate' so the deadlock check has a non-NULL threshold; m's
lock-in for Q11 (every agent suggestion needs the user's eye) means
bypassing the policy gate is correct here, not a regression.
- The shared `submit` kernel takes an optional agent_turn_id pointer.
All four lifecycle entry points (SubmitCreate / SubmitUpdate /
SubmitComplete / SubmitDelete) pass nil; SubmitAgentCreate passes
the turn id. INSERT to approval_requests now writes both
requester_kind + agent_turn_id atomically (xor-check on the schema
enforces consistency).
- models.ApprovalRequest grows the two columns + their JSON tags so
the inbox view + Verlauf renderer can read provenance without an
extra fetch.
- approvalRequestViewColumns adds ar.requester_kind + ar.agent_turn_id
to the SQL projection; both surfaces (ListPendingForApprover,
ListSubmittedByUser, GetRequest) inherit the new fields free.
- CreateDeadlineInput + CreateAppointmentInput each get an optional
AgentTurnID *uuid.UUID. When non-nil, the create-tx routes through
SubmitAgentCreate instead of the regular SubmitCreate. Default-zero
behaviour is unchanged for every existing caller.
- handlers/paliadin_suggest.go is the new HTTP layer. Owner-gated via
requirePaliadinOwner (same gate /paliadin uses), JSON-bodied,
RFC3339 + ISO-date validation, 409 + a useful message on
ErrNoQualifiedApprover.
- Project-event audit metadata gains requester_kind + agent_turn_id so
the project's Verlauf can render "Paliadin hat eine Frist
vorgeschlagen ✨" without joining approval_requests (Slice E reads
this).
SKILL.md (~/.claude/skills/paliadin/SKILL.md) gains an "Agent-suggested
writes" section with the tool catalog, behaviour rules ("never write
directly", confirmation in the response file, project_id lookup
discipline, RFC3339 dates, no chained tool calls per turn), and the
409 error contract.
go build + go vet + go test all clean. No frontend changes in this
slice — Slice E lights up the ✨ on existing eye-pill surfaces.
Refs: docs/design-paliadin-inline-2026-05-08.md §7.
34e82ea — Step 2 "Etwas einreichen" no longer drops straight into
Pathway A; it now shows a 3-card chooser:
File → Pathway A wizard (existing).
Draft → v1 placeholder, disabled button + "kommt bald" pill. No
/drafts route exists yet; the chooser slot is reserved.
Enter → /projects/<id>/deadlines/new, or /deadlines/new in ad-hoc.
Pathway type extends with "outgoing" intent.
Back-button policy:
Step 3a → Step 2 fork
Pathway A → Step 3a
Pathway B → Step 2 fork
m's 2026-05-08 18:09 spec: Step 3a is itself a 3-option fan-out. When
the user picks "Etwas einreichen" on Step 2 we no longer drop straight
into the Pathway A wizard; we ask "what kind of einreichen?" first.
Three cards:
- **File** (Schriftsatz einreichen) → navigates to Pathway A — the
existing wizard with proceeding picker, trigger date, flags,
timeline, save modal. The rule-library entry point.
- **Draft** (Schriftsatz entwerfen) → v1 placeholder. Disabled
button with a "kommt bald" pill in the corner. m specced this
as a link to a future drafting surface; for now we show the
intent without doing anything so the surface exists in the IA.
- **Enter** (Frist manuell erfassen) → routes to
`/projects/{id}/deadlines/new` (or `/deadlines/new` in ad-hoc
mode where there's no project to anchor against).
Pathway type extends to include "outgoing"; readPathwayFromURL +
showPathway both handle it. The Step 3a panel reuses .fristen-step2-
card visuals so File / Draft / Enter look consistent with the parent
Step 2 cards but distinct from Pathway A's proceeding picker.
Back-button policy:
- Step 3a back → Step 2 (the new "fork" state).
- Pathway A back → Step 3a (since that's where the user came from
in the new flow). Two clicks back to the fork.
- Pathway B back → fork directly (Step 2 happened-card jumps
straight to Pathway B; no intermediate chooser).
Out of scope for this slice:
- Step 3b's project-type-scoped event picker (Slice 3b).
- Klägerseite/Beklagtenseite role variants (Slice 3c).
- Real /drafts route — Draft stays a soft placeholder.
Refs t-paliad-157 / m/paliad#15.
dba8ad3 — feat(determinator/slice-2): /projects/new now honours a
?return=<path> query param. After a successful POST it bounces to
that path with ?project=<new_uuid> appended. Sanitization rejects
protocol-relative (//foo), absolute (https://…), and non-rooted
paths to avoid open-redirect.
Step 1 of the Determinator's "Neue Akte anlegen" link sends
?return=/tools/fristenrechner. Step 1's existing URL hydration
(Slice 1) picks up the ?project= and preselects — no new server
work needed.
The inline Paliadin chat surface — reachable from every authenticated
page, replacing the standalone /paliadin route as the primary entry
point. The standalone page survives as the dedicated full-screen mode
(the drawer's "↗ fullscreen" action links to it).
Components:
- frontend/src/components/PaliadinWidget.tsx — emits the floating
trigger button (bottom-right, lime ✨, owner-revealed by JS), a
scrim, and the right-edge slide-out drawer with header (reset /
fullscreen / close), context chip, message stream, empty-state
starter list, and textarea+send form. Loads /assets/paliadin-widget.js.
- frontend/src/client/paliadin-widget.ts — runtime. /api/me probe
reveals the trigger when caller matches PaliadinOwnerEmail (with
optional is_paliadin_owner flag fast-path); Cmd+J / Ctrl+J shortcut
toggles open/close (Cmd+K stays reserved for global search per
client/search.ts). Uses computePaliadinContext() (Slice B) per send
so route + entity + selection flow into every turn. SSE consumer
writes assistant bubbles; localStorage persists per-session history.
- frontend/src/client/paliadin-starters.ts — per-route starter prompt
registry. 14 routes covered (dashboard, projects.*, deadlines.*,
appointments.*, agenda, events, inbox, tools.*, glossary, courts) +
a _default fallback. Bilingual (DE/EN); prompts ending in `: ` seed
the textarea for the user to finish; fully-formed prompts auto-send.
- 39 authenticated TSX pages get a `<PaliadinWidget />` element after
`<Footer />` via a mechanical pass. paliadin.tsx (the standalone)
is intentionally excluded — its dedicated UI is the widget's
fullscreen escape hatch, not a place to overlay another widget.
- frontend/build.ts registers the new bundle.
- frontend/src/styles/global.css gains ~280 lines of widget CSS
(trigger / scrim / drawer / header / context-chip / messages /
bubbles / starters / form / send-btn) using only existing tokens.
Mobile (≤640px): drawer goes full-screen; trigger lifts above
bottom-nav slots.
- 11 new i18n keys × 2 langs = 22 entries under paliadin.widget.*.
Visibility predicate (paliadin-context.shouldSendContext) hides the
widget on /paliadin, /login, /onboarding. Owner-only gate stays on
PaliadinOwnerEmail.
Build clean: i18n 1955 → 1966 keys, IIFE-wrapped 218KB bundle, go test
green.
Refs: docs/design-paliadin-inline-2026-05-08.md §3, §5.
m's 2026-05-08 Slice 2: "Neue Akte anlegen" on the Fristenrechner now
round-trips cleanly. The Step 1 link sends `?return=/tools/fristenrechner`
on the way out; projects-new.ts honours the param after a successful
POST and redirects back with `?project=<new_uuid>` appended so the
just-created Akte preselects itself in Step 1.
Two pieces:
- frontend/src/client/projects-new.ts — new sanitizeReturnUrl()
rejects anything that could escape to a different origin
(protocol-relative `//foo`, absolute `https://...`, non-rooted
relative paths). On submit success, if a sanitized return URL
exists, build the destination via URL() so existing query params
on the return path stay intact and ?project= is set without
clobbering, then redirect there. Falls back to /projects/{id}
when no return param is present (existing behaviour preserved).
- frontend/src/fristenrechner.tsx — Step 1 link gets the
?return=/tools/fristenrechner query string so the bounce-back
knows where to land.
Step 1 hydration from Slice 1 already handles `?project=<uuid>` —
fetchProjects() repopulates cachedAkten, the projectId looks up its
ProjectOption record, renderStep1Summary() renders the collapsed
state, Step 2 cards become visible. No client-side state coordination
needed; the URL is the contract.
Refs t-paliad-157 / m/paliad#15.
df04e50 — feat(fristenrechner/determinator): the legacy "Was möchten
Sie tun?" landing fork is replaced by:
Step 1: filtered Akte picker + "Neue Akte anlegen" link (bare; the
bounce-back to the wizard after creation is Slice 2 scope) +
4 ad-hoc chips driving ?ad_hoc=upc|de|epa|dpma.
Step 2: "Etwas einreichen" / "Etwas ist passiert" cards driving
showPathway('a' | 'b'). Quick-pick chips moved here from the old
fork. Pathway A/B back buttons return to Step 2.
Save CTA on Pathway A's wizard disables in ad-hoc mode with hint
"Ad-hoc — kein Projekt, kein Speichern" (DE+EN). The locked context
collapses to a one-line summary; Reselect re-expands.
URL contract:
?project=<uuid> | ?ad_hoc=upc|de|epa|dpma — Step 1 result
?path=a|b — Step 2 result (back-compat)
?mode=tree|filter — Pathway B sub-mode
Pathway A/B sub-routing primitives (showPathway, showBMode) unchanged
— Step 2 cards just drive the same hooks.
Still open:
Slice 2 — /projects/new return-bounce on save.
Slice 3+ — scoping the picker / cascade by project's proceeding-type
+ role; replacing the wizard with the Step 3a File/Draft/Enter
chooser.
m's 2026-05-08 18:08 Determinator redesign Slice 1. Replaces the
legacy "Was möchten Sie tun?" fork (Pathway A vs B) with a two-step
funnel that puts the project (Akte) at the foundation:
Step 1 — Welche Akte?
- Filtered list of visible projects, search-as-you-type.
- "Neue Akte anlegen" link → /projects/new (bare; the bounce-back
with auto-preselect lands as Slice 2 per Maria's gating).
- Four ad-hoc explore-mode chips (Custom UPC / DE / EPA / DPMA
proceeding) for users who just want to look up a rule. No DB
write; URL becomes ?ad_hoc=upc|de|epa|dpma.
Step 2 — Was möchten Sie tun?
- Two cards: "Etwas einreichen" → Pathway A (Verfahrensablauf
wizard) and "Etwas ist passiert" → Pathway B (cascade, mode=tree).
- Quick-pick chips moved here from the old fork's shortcut row.
Once Step 1 picks a context, the picker collapses to a one-line
summary "Akte: X · [Andere Akte]" mirroring the proceeding-summary
collapse pattern (097e21c). Reselect re-expands and clears downstream
state.
State on URL:
?project=<uuid> project context
?ad_hoc=upc|... ad-hoc explore-mode
?path=a|b Step 2 outcome (kept for back-compat)
?mode=tree|filter Pathway B sub-mode (kept)
The legacy back-from-Pathway buttons now return to Step 2 (the new
"fork" state). showPathway() / showBMode() unchanged — Step 2 cards
just drive the same primitive.
Save-to-project CTA on Pathway A's wizard detects ad-hoc mode and
disables itself with the hint "Ad-hoc — kein Projekt, kein Speichern"
(EN: "Ad-hoc — no matter, no save"). Hiding the CTA would leave the
user wondering where the action went; disabling makes the constraint
legible (per m's lock #2).
Frontend pieces:
- fristenrechner.tsx — Step 1 + Step 2 markup; legacy
fristen-pathway-fork removed wholesale.
- client/fristenrechner.ts — new Step1Context type + URL hydration
+ render helpers; initPathwayFork rewired to drive the new
cards; renderProcedureResults gates the save CTA on
isAdhocMode().
- client/i18n.ts — 19 new keys (DE+EN) under deadlines.step1.* +
deadlines.step2.* + the save CTA hint.
- styles/global.css — .fristen-step1 / .fristen-step2 block + chip
+ summary styles, all bound to the existing --color-* token
palette. Mobile breakpoint stacks the Step 2 cards at <600px.
Out of scope for this slice (will land later):
- Slice 2: /projects/new bounce-back with auto-preselect via
?return=/tools/fristenrechner.
- Slice 3+: scoping the picker / cascade by project's
proceeding-type + role; replacing the existing wizard with the
Step 3a "File / Draft / Enter" chooser.
Refs t-paliad-157 / m/paliad#15.
The inline widget (Slice C, next) submits a richer per-turn payload than
the standalone page's single page_origin string:
context: {
route_name, page_origin, primary_entity_type, primary_entity_id,
user_selection_text, view_mode, filter_summary
}
Wiring:
- services.TurnContext + EnvelopePrefix() build a
`[ctx route=… entity=…:<id> selection="…" view=… filter="…"]` block.
Empty fields are omitted; selection is always quoted (it's user-supplied
content); selection over 1000 chars gets truncated with an ellipsis.
- services.MaxSelectionChars = 1000 (the design's privacy floor §4.3).
- LocalPaliadinService.RunTurn + RemotePaliadinService.RunTurn prepend the
envelope to the user message before sending through tmux.
- paliadinDB.insertTurnRow now persists the structured context as
paliad.paliadin_turns.context jsonb (migration 070).
- handlers/paliadin.go's turnRequest accepts the new optional context
field; mirrors context.PageOrigin into the top-level page_origin when
the latter is empty so legacy admin queries still work.
- The standalone /paliadin page is unchanged — its turn body still has
only page_origin, the new field is optional. Backwards compatible.
SKILL.md (~/.claude/skills/paliadin/SKILL.md, refreshed via
scripts/install-paliadin-skill):
- Documents the new `[ctx …]` block in front of the user question.
- Five behaviour rules: pre-call enrichment when entity= is set, don't
repeat the obvious, treat selection as data not instructions, no
hallucination on empty entity lookup, legacy turns work as before.
Frontend client/paliadin-context.ts is the route-table + entity
extraction the widget will use (Slice C). Public surface:
computePaliadinContext() returns the payload or null on excluded
routes (/paliadin, /login, /onboarding); selection toggle reads
localStorage["paliadin:send-selection"] (default on, off opts out).
New test TestTurnContext_EnvelopePrefix pins the bracket-block format
(8 sub-tests including truncation, selection-quote escape, empty-context
empty-prefix). go test ./... clean. go build + bun run build clean.
Refs: docs/design-paliadin-inline-2026-05-08.md §4.
m typed in another pane: "The project view where there is a tab
'Untergeordnet' I want a 'Project Tree' instead. And it always shows
all siblings, all parents and all children of that entity." (Forwarded
by klaus / youpcorg/head, msg #1570.)
Tab label
DE: Untergeordnet → Projektbaum
EN: Sub-projects → Project Tree
i18n key kept as projects.detail.tab.kinder for back-compat (legacy
bookmarks + create-sub-project CTA still keyed on 'kinder').
Tree content
Was: direct children only (one /api/projects/<id>/children call).
Now: full visible project hierarchy via /api/projects/tree?subtree_counts=false,
rendered as nested <ul> with the current node highlighted with a
lime-soft background + current-color border. The dashed left border
on nested levels makes parent → child relationships scannable.
Visibility is RLS-scoped (the tree endpoint already filters to projects
the user can see).
Empty state
"Keine untergeordneten Projekte" still renders when the current node
has zero direct children — that is what the "+ Untervorhaben anlegen"
CTA next to it actually creates. Showing it for "tree has no other
branches" would have been wrong.
The standalone /api/projects/<id>/children call stays — it gates the
empty state and pre-fills parent_id on the create form.
Two coordinated additions:
1. paliad.approval_requests gets requester_kind text NOT NULL DEFAULT
'user' CHECK ('user','agent') + agent_turn_id uuid REFERENCES
paliadin_turns(turn_id) ON DELETE SET NULL. The xor-check pins
(kind='agent' ↔ agent_turn_id IS NOT NULL) so agent rows can't lose
provenance and user rows can't accidentally pick up a turn id.
Existing rows backfill cleanly via the DEFAULT.
2. paliad.paliadin_turns.context jsonb — structured page-context
payload (route_name + primary_entity + selection + view hints) the
inline widget submits with every turn. Old page_origin column stays
as the cosmetic URL field.
Idempotent — every ALTER uses IF NOT EXISTS and the constraints/index
are guarded by DO blocks. Verified live via BEGIN..ROLLBACK on the
production DB: cols + 3 constraints + index land cleanly, second apply
is a no-op, xor-check rejects ('agent', NULL).
Skipped the optional PaliadinRelay interface extraction per the design
doc's own §6.4 caveat: paliadin.go + paliadin_remote.go already share
the paliadinDB substrate cleanly; introducing an interface now would
duplicate without removing duplication. Reserves the seam for the
future API cutover without paying its cost today.
Refs: docs/design-paliadin-inline-2026-05-08.md §7.1, §4.2, §6.4.
Two intertwined Paliadin upgrades, scoped together because the chat
surface is where the write path is triggered and the write path is what
makes the chat non-trivial:
1. Inline slide-out modal reachable from every authenticated paliad
page, with structured page-context payload (route_name +
primary_entity + selection text) and per-route starter prompts.
2. Agent-suggested write path that drafts deadlines/appointments/notes
into the existing pending_create lifecycle (t-paliad-160) with new
provenance columns on approval_requests (requester_kind + agent_turn_id);
approved-from-agent rows render alongside 👀 with a sparkle ✨.
Hard call: keep the existing tmux relay for v1; recommend (but do not
commit) the Anthropic API cutover as a prerequisite for opening beyond
owner-only. Single Paliadin persona — no scope-bouncer pre-design.
Inventor parked. DESIGN READY FOR REVIEW. Awaiting m's go/no-go before
any coder shift.
Refs: m/paliad#20, t-paliad-146, t-paliad-160, t-paliad-138.
fdbbc74 — feat(fristenrechner/cascade): more opponent-side proceeding
types in the B1 cascade. Adds 5 parents (UPC Berufung, UPC einstweilige
Maßnahmen, DE BGH Revision/NZB, DE BGH Berufung Nichtigkeit, DPMA
Rechtsbeschwerde) + 17 leaves under cms-eingang.gegenseite. Forums-
tagged for the inbox-channel chip and proceeding_type_code-narrowed
so result cards land on the right rules. Migration is idempotent
(ON CONFLICT clauses).
Live tracker is already at v69 (feynman applied during dev). Deploy
will see tracker=69 with file 069 present and have nothing to apply.
Role variants (Klägerseite vs Beklagtenseite) deferred to the
Determinator redesign per m's earlier scope decision.
m's 2026-05-08 17:41 batch Item 4: today
`cms-eingang.gegenseite` exposes UPC INF/REV, DE INF/NULL, EPA OPP/APP,
DPMA OPP — but is missing the appellate / interim-measures arms m
named. m: "we need a lot more proceeding types for Opponent submission
— we currently only see Verletzungsverfahren."
Migration 069 adds 5 new parent nodes and 17 leaves under
`cms-eingang.gegenseite`:
- upc-app UPC Berufungsverfahren — 5 leaves: Berufungs-
schrift, -begründung, -erwiderung, Anschluss-
berufung (R.237), Erwiderung dazu (R.238).
Concepts: notice-of-appeal, statement-of-
grounds-of-appeal, response-to-appeal,
cross-appeal, reply-to-cross-appeal.
- upc-pi UPC einstweilige Maßnahmen — 2 leaves: PI-
Antrag, PI-Erwiderung. Concepts: application-
for-provisional-measures, statement-of-defence.
- de-bgh-inf DE Revision / NZB BGH (Verletzung) — 5 leaves:
NZB, NZB-Begründung, Revisionsschrift,
Revisionsbegründung, Revisionserwiderung.
Concepts: nichtzulassungsbeschwerde,
nichtzulassungsbeschwerde-begruendung,
revisionsfrist, revisionsbegruendung,
response-to-appeal.
- de-bgh-null DE Berufung BGH (Nichtigkeit) — 3 leaves:
Berufungsschrift, -begründung, -erwiderung.
Concepts: notice-of-appeal,
statement-of-grounds-of-appeal,
response-to-appeal.
- dpma-bgh DPMA Rechtsbeschwerde BGH — 2 leaves:
Rechtsbeschwerde, RB-Begründung. Concepts:
rechtsbeschwerde, rechtsbeschwerde-begruendung.
Each parent + leaf carries the matching forums tag so the
inbox-channel chip (#15) hides / shows the subtree correctly. The
event_category_concepts junction sets proceeding_type_code per leaf
(UPC_APP / UPC_PI / DE_INF_BGH / DE_NULL_BGH / DPMA_BGH_RB) so the
result card pills only the relevant proceeding's rules.
All INSERTs use ON CONFLICT (slug) DO UPDATE so re-running the
migration after a partial apply is safe. Mig applied to live Supabase;
tracker at v69.
Refs m/paliad#15.
Three commits from mai/feynman/fristenrechner:
- 614f9af fix(approval-pill): two-eyes glyph 👀 instead of single SVG eye
on /deadlines + /appointments + /agenda. m's preference: emoji denotes
"being looked at" closer to "wartet auf Genehmigung" semantics.
- 2d6ea3e feat(deadline-rules/is-optional): conditional rules opt-in via
save modal. Adds paliad.deadline_rules.is_optional. Distinct from
is_mandatory: a rule can be statutorily fixed when it applies AND
conditional on whether it applies (RoP.151 cost-decision request,
appeal-related deadlines). Save-modal pre-unchecks optional rows;
user toggles to opt in. Timeline shows "auf Antrag" pill.
- 097e21c feat(fristenrechner): proceeding-picker collapses to one-line
"Verfahren: X · [Reselect]" pill after pick (saves vertical space).
Column view becomes the default for the timeline (was previously
whichever-default; m wants Column on first render).
Migration housekeeping:
feynman's migration was authored as 066 on his branch but main has
already taken 066/067 via shannon's t-paliad-160 (approval policy
split + drop required_role). Renumbered to 068 during merge to
resolve the same-number collision. Added ADD COLUMN IF NOT EXISTS
to make the up-migration idempotent (defensive for environments
where the column was already applied out-of-band during dev). The
RoP.151 backfill UPDATE is naturally idempotent.
Live tracker bumped from 66 → 68 to reflect schema reality before
this merge: shannon's 066+067 effects and feynman's is_optional
column are all already present in the live youpc Supabase. The
next deploy will see tracker=68 and have nothing to apply.
Refs m/paliad#15, m/paliad#18 (rule-Typ contradiction filed against
Item A scope, not part of this batch).
m's 2026-05-08 18:26 dogfood batch — two pure UX tweaks on the
Verfahrensablauf wizard:
1) Collapse the proceeding-picker once a Verfahren is chosen. Replaces
the four-group block (UPC / DE / EPA / DPMA, ~25 buttons total)
with a one-line "Verfahren: X · [Anderes Verfahren wählen]" pill.
Reselect re-expands without throwing away the rest of the wizard
state (trigger date, flags, calc result stay put until the user
actually picks again). reset() also re-expands.
2) Column view as the default for step 3. The proactive / court /
reactive grid reads more naturally for the HLC team than the
single vertical timeline. URL semantics flipped: ?view=timeline
now opts back into the legacy view; absence of ?view= yields
columns. Share links stay clean.
Files:
- frontend/src/fristenrechner.tsx — new .proceeding-summary
markup; the view-toggle radio order swapped so "Spalten" is the
first / checked option.
- frontend/src/client/fristenrechner.ts — setProceedingPickerCollapsed
helper toggles the four .proceeding-group blocks vs the summary;
selectProceeding collapses, reset() + Reselect re-expand.
procedureView default flipped to "columns"; initViewToggle URL
semantics inverted.
- frontend/src/client/i18n.ts — 2 new keys (DE+EN) for the
summary label + Reselect button.
- frontend/src/styles/global.css — .proceeding-summary +
.proceeding-summary-reselect styles, all bound to the existing
--color-* token palette.
Refs m/paliad#15 dogfood thread (m's 2026-05-08 18:26 batch).
m's 2026-05-08 batch Item 2: some rules don't always apply per-instance.
Antrag auf Kostenentscheidung (RoP.151) only fires when a party files
for it; some appeal-related deadlines depend on specific facts. Today
they render in the timeline as if always applicable; the save-to-
project modal pre-checks them so the user has to remember to uncheck.
New paliad.deadline_rules.is_optional bool flag (default false). Threads
through the Go model, ruleColumns SELECT, UIDeadline JSON, and the
frontend save modal:
- Migration 066 adds the column + comment + a starter UPDATE that
flips RoP.151 to is_optional=true. m can flip more via SQL as he
reviews the rule library — distinct from is_mandatory, which is
about statutory strictness once the rule applies (an "auf Antrag"
rule can be is_mandatory=true once requested).
- Save modal: optional rows pre-uncheck (the user opts in) and a
small "auf Antrag" / "on request" pill renders in the meta line.
Court-determined rows still pre-uncheck via the existing disabled
path; isOptional doesn't override that.
Migration applied to live Supabase; tracker at v66.
Refs m/paliad#15 (m's 2026-05-08 18:21 follow-up batch Item 2).
m's 2026-05-08 18:21 follow-up: "two eyes instead of the one." The
single-eye SVG read as a watching-Eye-of-Sauron glyph; 👀 reads as
"under review" / "being looked at" — closer to "wartet auf
Genehmigung" semantics.
Drops the inline SVG + the .approval-pill--icon svg sizing rule;
replaces with the literal emoji as the pill's text content. CSS
modifier becomes a small auto-width text pill (min-width 28px so
single emoji stays nicely round-ish at higher densities).
Renamed APPROVAL_PILL_EYE_SVG → APPROVAL_PILL_GLYPH in both events.ts
and agenda.ts since the constant is no longer SVG.
m's 2026-05-08 cosmetic ask: the "Wartet auf Genehmigung" badge ate
row width and read as a noisy block of text on every pending row.
Replace with a 22px eye-icon pill; the lifecycle label moves to the
hover tooltip (title attr + aria-label so screen readers still get
the full text).
Three pieces:
- global.css — new .approval-pill--icon modifier sets the pill to
a circular 22×22 hit target with a centered SVG. Base
.approval-pill (text-pill behavior) and --historic (inbox status
pill) stay untouched so the inbox surface keeps rendering the
full status + decider name.
- client/events.ts (the /deadlines + /appointments shell) and
client/agenda.ts each get a tiny APPROVAL_PILL_EYE_SVG constant
+ the new --icon class on the pending pill. Two definitions
(no shared icons module today; no other surfaces need this glyph
yet) — the duplication is two lines, easier to read than yet
another import.
What it looks like: 👁 in a soft amber circle, hovers to "Änderung
wartet auf Genehmigung" / "Erledigung wartet auf Genehmigung" / etc.
The lifecycle-specific label kept (no schema work) — Maria gated this
slice as pure-frontend; the richer "wartet auf Genehmigung von
<role>; angefragt am <date>" tooltip needs a backend join we're not
doing here.
Refs t-paliad-160 §C / m's 2026-05-08 18:15 batch Item B.
m's 2026-05-08 17:50 feedback: 'Antrag auf Kostenentscheidung' (RoP.151)
labels itself "wird vom Gericht bestimmt" but the rule is actually
"1 Monat ab Hauptentscheidung". The court doesn't directly determine
this date — it determines the parent's date (Hauptentscheidung) and
this rule chains off that. Calling it "vom Gericht bestimmt" overstates
the relationship; "unbestimmt" reads correctly: derived from a
not-yet-known anchor.
Two failure modes split:
- Direct court-set rule itself is hearing / decision / order
(or primary_party='court'). Label stays
"wird vom Gericht bestimmt" — strictly correct.
- Indirect court-set rule has a real duration but its anchor is a
court-set parent (RoP.151 case), or it's a
zero-duration rule whose parent is court-set
without a real date. Label flips to
"unbestimmt".
Backend: new `IsCourtSetIndirect bool` on UIDeadline, set on the three
indirect cases inside FristenrechnerService.Calculate. Direct cases
keep IsCourtSetIndirect=false so their label stays unchanged. JSON
omits the field when false, no consumer churn.
Frontend: deadlineCardHtml + the save-modal row both consult
IsCourtSetIndirect to pick between two i18n keys (deadlines.court.set
"vom Gericht bestimmt" and deadlines.court.indirect "unbestimmt"; EN
falls back to "set by court" / "tbd"). The override edit affordance
keeps working unchanged — user types the actual parent date, downstream
re-flows.
Refs m/paliad#15 (m's 2026-05-08 17:50 feedback Item 1).
m's 2026-05-08 feedback: the inbox-channel chip is a Determinator step,
not a page-level prefilter — "Verlauf does not need to see that so it
cant be outside of that."
Changes:
- frontend/src/fristenrechner.tsx — strip the .fristen-inbox-bar
markup from above the pathway fork; mount it instead at the top
of #fristen-b1-panel, before the cascade. The chip is now visible
only when the user enters Pathway B → tree mode.
- frontend/src/client/fristenrechner.ts — drop the .proceeding-group
visibility loop from applyInboxFilter. Pathway A's wizard is no
longer filtered by the chip. The data-forum attributes stay on
the markup as documentation of intent but no longer drive
visibility.
What stays:
- persistence (paliad.users.forum_pref via PATCH /api/me)
- URL ?inbox= override
- B1 cascade narrowing via paliad.event_categories.forums
- B2 fine-bucket activeForums sync (B2 lives inside the
Determinator too)
Refs m/paliad#15 (m's 2026-05-08 17:50 feedback).