shape-timeline.ts:
- Renders Kind="projected" rows with Status-driven styling: predicted
(faded grey), court_set (dashed border), predicted_overdue (amber
fade with overdue glyph).
- "[Datum setzen]" inline date editor on every projected row with a
rule_code. Submit POSTs /api/projects/{id}/timeline/anchor; 200
triggers onChange (re-fetch + re-render); 409 renders the
predecessor_missing payload as inline error with a "Stattdessen
<predecessor> erfassen" link that scrolls to + opens the parent's
editor.
- "Folgt aus: <Name> (<Code>, <Date|Datum offen>)" footer on every row
with depends_on_rule_code, plus "[Pfad anzeigen]" expander hint.
- "[+ Mehr anzeigen]" / "[− Weniger]" lookahead toggle when backend's
X-Projection-Total header indicates more projections exist beyond
the current cap.
- Status pills on projected rows surface the status nuance next to
the kind chip without overwhelming the title.
projects-detail.ts:
- loadTimeline reads X-Projection-{Total,Lookahead} headers and forwards
them to renderSmartTimeline.
- Lookahead state persisted in localStorage per project (key
`paliad.smarttimeline.lookahead.<id>`).
- Removes the renderEvents() orphan (band-aid from t-paliad-172) and
every call site — renderTimeline is the only project-page render
path now. Aligns with fermat's commit-message hint in 0835be4.
FilterBar (substrate):
- New axes timeline_status / timeline_track (chip clusters, multi-
select). Macro chip pair "Zukunft anzeigen" / "Nur vergangenes" on
the timeline_status axis maps to the predicted+court_set subset
on/off.
- url-codec round-trips ?tl_status= / ?tl_track= so saved Sichten /
bookmarks survive.
CSS:
- ~80 LoC for .smart-timeline-row--projected/--court_set/--predicted_overdue,
status pills, depends-on footer, anchor editor, lookahead toggle.
All tokens reuse existing CSS variables — no bare-hex fallbacks
(cf. t-paliad-150 dark-mode lesson).
i18n:
- 31 new keys (DE+EN) for projected statuses, depends-on labels,
anchor editor states, lookahead chips, FilterBar axis labels +
values + macro chips. 2102 → 2146 total.
Tests:
- projection_anchor_test.go covers applyLookaheadCap (overdue +
court_set exemption), applyLookaheadDefault clamping,
ruleAnchorKind dispatch, extractMetadataString, lang normalisation,
ruleNameInLang, PredecessorMissingError unwrap, annotateDependsOn
(including parent-of-parent chain dating).
Migration 076 was applied live during dev (tracker 75 → 76); deploy
re-applies idempotently via the embedded migrate path.
Slice 1 of SmartTimeline (t-paliad-171, commit 7057fe5) removed the
legacy <ul#project-events-list> + <p#project-events-empty> markup from
projects-detail.tsx but didn't prune the renderEvents() call sites. The
function still runs from main() and several other paths; with non-null
assertions on getElementById, the null deref threw a TypeError mid-init.
The throw aborted main() between body.style.display = "" and initTabs(),
so the .entity-tab click handlers were never attached. Default-action
clicks on <a href="#"> just appended "#" to the URL while the user was
already viewing whatever panel happened to be the default-display
section (tab-history) — making the Verlauf tab feel "stuck" because the
visible panel never changed.
Fix: drop the non-null assertions, null-guard the legacy DOM lookups,
and return early when the targets are gone. renderEvents() becomes a
silent no-op in the SmartTimeline layout, which matches euler's intent
documented in 7057fe5: "The legacy renderEvents() rendering path stays
as-is (dead, but the function is still called in places). It will be
removed once /timeline?include=audit_full has had a deploy of soak time
… Slice 2 revisits."
Verified locally with the projects-detail.js bundle + a fetch mock:
clicks on Team / Projektbaum / Parteien / etc. now switch the active
tab and panel display, the URL updates via replaceState, the
SmartTimeline still renders its empty state, and the "+ Eintrag" modal
still opens and closes correctly.
Replaces the legacy <ul.entity-events> Verlauf rendering with the new
SmartTimeline. Slice 1 wiring:
- loadTimeline(id) calls /api/projects/{id}/timeline (the new
endpoint backed by ProjectionService) and renderSmartTimeline
paints into <div#project-smart-timeline>.
- "Audit-Log anzeigen" header toggle re-fetches with
?include=audit_full, broadening the project_events filter to
every audit row (legacy Verlauf chronological view). State
persists per-project in localStorage so flipping it on for one
case doesn't carry across to others.
- "+ Eintrag" CTA opens a modal. "Eigener Meilenstein" submits
via POST /api/projects/{id}/timeline/milestone and re-renders;
Frist + Termin route to the existing /deadlines/new and
/appointments/new flows; CCR + R.30 are disabled-with-tooltip
"kommt mit Slice 3" per the design.
- Subtree toggle now also drives the timeline (passes
?direct_only=true when the user flips off "Inkl. Unterprojekte").
- Project-appointment add path also re-fetches the timeline so the
new appointment surfaces immediately.
The legacy renderEvents() rendering path stays as-is (dead, but the
function is still called in places). It will be removed once
/timeline?include=audit_full has had a deploy of soak time and the
audit-toggle is the only path that feeds the legacy markup. Slice 2
revisits.
The FilterBar from t-paliad-170 (riemann's port) keeps mounting and
driving its customRunner — facets still narrow the legacy `events`
array. The bar gaining timeline_* axes lands later in the slice
sequence (design §8); Slice 1 ships the timeline beneath the existing
bar untouched.
Design ref: docs/design-smart-timeline-2026-05-08.md §10 Slice 1.
The vertical-timeline render component for the SmartTimeline (Verlauf
tab redesign). Two-column layout (date / event card), past
chronological → "Heute →" rule → future chronological, status icon +
kind chip per row.
Deep-link is wired via a row-level click handler that skips clicks on
inner <a>/<button>, NOT a ::before overlay — matches the project's
.entity-event whole-card click contract (project CLAUDE.md), keeps
text selection working, and avoids the t-102 overlay regression that
swallowed pointer events on the title text.
i18n: 28 new keys under projects.detail.smarttimeline.* (DE primary,
EN secondary). i18n-keys.ts is regenerated by build.ts on every build,
so the diff there is mechanical.
CSS: ~250 LoC under .smart-timeline-* — vertical layout, status-icon
glyphs per status (✓/…/!/▢/░/⊕), kind-chip pastels, Heute → rule with
borders extending into the spacing.
Design ref: docs/design-smart-timeline-2026-05-08.md §3.1-3.3.
riemann's Phase 2 slice on top of own 1faffb6 Phase 1: the universal
<FilterBar> is now in the project Verlauf tab. Filter facets:
project_event_kind (chip cluster), time (presets including new
HorizonPast7d), personal_only. Empty URL preserves current behaviour
(unfiltered list); ?time=past_30d&pe_kind=deadline_created narrows.
Two extension points added to the bar primitive (forward-compat with
SmartTimeline t-paliad-169 work):
- customRunner: lets a host page own the data fetch (Verlauf keeps
the legacy /api/projects/{id}/events pipeline so subtree + cursor
pagination survive — substrate-side scope-with-descendants stays
SmartTimeline territory).
- timePresets: opt-in past-only horizon set for backward-looking
surfaces (vs the default future-leaning set used on /inbox).
3-way merge with main: clean. fourier's t-paliad-168 + lagrange's
SmartTimeline design doc preserved.
bun build clean; frontend/dist regenerated. go test internal/... ok
on riemann's worktree (filter-bar url-codec + filter_spec tests).
ebcda13 from mai/riemann/filterbar-phase-2-slice.
Phase 2 slice of the universal-filter migration (Phase 1 was
t-paliad-163 → /inbox; remaining /agenda /events /deadlines
/appointments stay queued).
What ships:
- FilterBar gains two non-invasive options that future surfaces will
also need:
customRunner — bypass the substrate POST and hand the effective
spec to a surface-supplied runner. Required by
surfaces whose data path can't move to the substrate
yet (Verlauf still uses /api/projects/{id}/events for
subtree expansion + cursor pagination, both absent
from the substrate's project_event runner).
timePresets — per-surface override of the time chip cluster, so
backward-looking surfaces can show past_*+all without
forcing forward-looking next_* chips on every host.
systemViewSlug becomes optional; the bar enforces "exactly one of
customRunner | systemViewSlug" at construction.
- project_event_kind axis renderer (was a null stub) — chip cluster
over KnownProjectEventKinds, labels reuse the existing
event.title.<kind> i18n table so the chip text matches the Verlauf
row title for the same kind.
- HorizonPast7d added end-to-end (substrate validate +
computeViewSpecBounds; FilterBar TimeOverlay + parseHorizon; views
TimeHorizon mirror) so the chip value is valid in every layer when a
later SystemView reuses it.
- Verlauf tab on /projects/<id> mounts the bar with
axes=["time","project_event_kind"], timePresets=
["past_7d","past_30d","past_90d","any"], showSaveAsView=false. The
customRunner reads predicates.project_event.event_types + time.horizon
off the effective spec, sets a verlaufFilters global, and routes
through the legacy loadEvents/loadMoreEvents pipeline (which now
applies the filter set client-side and tracks raw cursor IDs so
"Mehr laden" still walks the underlying pagination boundary even when
most rows get filtered out of a page).
- Subtree toggle drives loadEvents through verlaufBar.refresh() so the
current filter state survives the toggle.
URL state reuses the bar's existing keys (?time=past_30d, ?pe_kind=…).
Empty filter → identity passthrough → current behaviour preserved.
Out of scope (deferred to t-paliad-169 SmartTimeline):
- Migrating Verlauf to the substrate (needs scope-with-descendants)
- Past/future split, dated/undated split, source-track facet
Refs m/paliad#23.
t-paliad-168 deliverable 2. New "Verfahrensablauf" entry under
Werkzeuge, right after Fristenrechner — opens
/tools/fristenrechner?path=a (Pathway A wizard, browse-/learn-mode).
Uses a distinct open-book icon to read separate from the closed-book
Glossar. Both /tools/fristenrechner sidebar entries share the same
pathname, so SSR navItem matching can't pick the right "active" one
on its own — fixVerfahrensablaufActive() in sidebar.ts disambiguates
based on ?path=a at hydration.
i18n key: nav.verfahrensablauf (DE: "Verfahrensablauf",
EN: "Procedure Roadmap"). i18n-keys.ts is regenerated by build.ts.
t-paliad-168 deliverable 1. Adds a discoverable browse-/learn-mode
entry to the determinator alongside "Etwas einreichen" / "Etwas ist
passiert". Click drops straight into Pathway A's proceeding-tile
picker (navigateToPathway("a")).
The save-to-project CTA disables itself in this mode — extends
isAdhocMode() to also return true when no Step 1 context is set,
mirroring the existing ad-hoc explore behaviour.
i18n keys: deadlines.step2.browse.title / .desc (DE + EN).
m's 2026-05-08 22:08 dogfood: my first auto-fill landed but kept Regel
and Typ as TWO separate input fields. m wanted ONE — "these two are
connected, it's the same thing".
Now: when a Regel is selected and the rule's concept resolves to a
canonical event_type via the jurisdiction-aware junction, the Typ
chip cluster is HIDDEN and replaced by an inline summary —
Klageerwiderung (vorgegeben durch Regel) Anderen Typ wählen
Clicking "Anderen Typ wählen" sets a sticky expandedOverride flag
that forces the picker visible for the rest of the form session.
The chip stays in the picker so the user can edit / remove it.
The picker also stays visible when the rule has no canonical
event_type (fallback to free-text Typ) or when the user has picked
a different event_type from the canonical default (mismatch
warning surfaces yellow next to the picker, never blocking).
DE+EN i18n: deadlines.field.rule.{autofill_inline,override}.
New CSS: .event-type-collapsed{,-label,-source,-override} reusing
the existing lime-tint chip palette.
m's 2026-05-08 22:08 dogfood, after t-paliad-163 Phase 1: "I like the
new inbox filters but now the inbox somehow does not show nothing no
more..." The new bar opened with the chip defaulted to
"approver_eligible" (the legacy "Zur Genehmigung" tab semantics —
requests EXCLUDING ones the caller authored). For users who only
SUBMIT requests and have nothing to approve themselves (incl. m,
who has 4 own pending submissions and 0 incoming), that's an empty
view.
Flip the default to "any_visible" on both ends:
- internal/services/system_views.go InboxSystemView.Filter — base
spec ViewerRole = "any_visible".
- frontend/src/client/filter-bar/axes.ts approval_viewer_role chip —
default = "any_visible" when the URL doesn't pin one. The two
defaults are intentionally redundant: the server narrows on its
default if the request omits a_role, and the chip highlights the
same option on the empty URL.
The chip still narrows. "Zur Genehmigung" + "Eigene Anfragen" stay
one click away; the bar just doesn't pre-narrow into "Zur Genehmigung"
on first visit anymore.
The "/views/inbox-mine" SystemView (slug + URL stays "self_requested")
keeps its narrower default — that route exists precisely to land on
the requester's view.
Three slices on mai/riemann/inventor-universal:
d5a01e6 Slice 1 — RenderSpec.list.row_action + validator + tests
de4e133 Slice 2 — <FilterBar> scaffolding (axes / url-codec / save-modal)
4670cd6 Slice 3 — /inbox migrates to <FilterBar>; tabs collapse to chips
What ships (Phase 1):
- A new frontend/src/client/filter-bar/ module:
types.ts — Spec + RenderSpec + AxisDeclaration types
axes.ts — registry of supported filter axes
url-codec.ts — URL ↔ FilterSpec serialization (round-tripping)
save-modal.ts — "Speichern als Sicht" dialog
index.ts — <FilterBar> mounts
Plus a url-codec.test.ts golden table.
- /inbox surface migrates to the bar:
Top-level "Zur Genehmigung / Meine Anfragen" tabs collapse into the
bar's `approval_viewer_role` chip cluster (incoming / outgoing /
both). One control, three mutually exclusive options. Stateful via
`?role=` URL param.
Bookmark-friendly: legacy `?tab=mine` + `?tab=pending-mine` redirect
to `?role=outgoing` and `?role=incoming` respectively for one
release.
Sortable column headers on the result list (list-shape only;
cards/calendar shape-modes defer their own ordering to the spec).
- RenderSpec.list gains `row_action` ("navigate" | "expand" | "none")
so list-shape surfaces declare row click behaviour explicitly. The
validator + tests cover the new field.
- system_views.go gains the inbox SystemView definitions so the bar
reads its base spec from the same registry that custom views use.
m's locked positions (commit `1e23745` design doc; m's greenlight
2026-05-08 21:47): all 11 default picks honoured. Q4 = collapse
tabs to chips ✓.
Phase 2 surfaces (port /agenda → bar; port /events → bar; port
/deadlines → bar; port /appointments → bar) follow as separate PRs.
Refs m/paliad#23.
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.
/inbox is the first surface to consume the universal FilterBar. The
two-tab UI collapses into the bar's approval_viewer_role chip cluster
(per Q4 lock-in 2026-05-08 21:47); status / entity_type / time chips
are new affordances; density toggle gives the activity-feed look the
brief asked for.
Changes:
- system_views.go: InboxSystemView + InboxRequesterSystemView render
spec gains RowAction=approve so shape-list.ts knows which row
layout to stamp (entity title + diff + approve/reject/revoke).
- shape-list.ts: row_action='approve' branch — stamps the inbox-row
markup the surface owned today; surface attaches click handlers
via data-attrs on .views-approval-action / .views-approval-row.
- inbox.tsx: tab row replaced with <div id='inbox-filter-bar'> +
<div id='inbox-results'>. Heading + admin nudge unchanged.
- client/inbox.ts: shrunk to mountFilterBar with axes [time,
approval_viewer_role, approval_status, approval_entity_type,
density, sort]. Action handlers run via fetch + bar.refresh().
Legacy ?tab=mine -> ?a_role=self_requested redirect on mount so
bookmarks / sidebar bell still land on the right sub-view.
Build clean: bun run build + go build/vet/test all pass.
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.
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.
Schema bump that lets the universal <FilterBar> tell shape-list which
row interaction to wire (navigate / complete_toggle / approve / none).
Defaults to navigate when empty so existing SystemView definitions and
saved user views continue to render rows that route to the per-kind
detail page.
Validator extended; pure-Go test cases over every enum value + reject.
TS mirror updated in client/views/types.ts. No DB migration — the
field is purely additive on the JSON shape.
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 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.
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 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.
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.
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.
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).