1844df3ae6589a309b4ca482a9ce8165ade78248
182 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 251f5a250f |
feat(t-paliad-182): mig 078 — unified rule columns
Phase 3 Slice 1 Step A (design §3.1). Additive only; no drops, no data change. Adds nine columns to paliad.deadline_rules so the calculator + rule editor can converge on a single rule shape over the following slices: trigger_event_id (bigint, FK trigger_events.id) spawn_proceeding_type_id (int, FK proceeding_types.id) combine_op (text, CHECK 'max'|'min') condition_expr (jsonb) priority (text, DEFAULT 'mandatory', 4-way CHECK) is_court_set (bool, DEFAULT false) lifecycle_state (text, DEFAULT 'published', 3-way CHECK) draft_of (uuid, self-FK) published_at (timestamptz) FK types follow the actual referenced columns (bigint on trigger_events, int4 serial on proceeding_types) — the design doc's "int FK" shorthand is widened to the precise widths. FKs are DEFERRABLE INITIALLY IMMEDIATE so Slice 3's data-move can defer FK checks within a single transaction without disturbing normal-statement semantics. Indexes: partial WHERE NOT NULL on the two FK columns (sparse; most rules have neither); plain btree on lifecycle_state so the admin filter on 'published' is O(log n). |
|||
|
|
306bb11618 |
feat(t-paliad-174): SmartTimeline Slice 3 — counterclaim sub-project schema + service
Migration 077 adds paliad.projects.counterclaim_of (nullable FK ON DELETE SET NULL) plus a partial index. A trigger function rejects two-level CCR chains: a project with counterclaim_of NOT NULL cannot be the target of another CCR — UPC practice has no CCR-of-a-CCR shape, so reject it at the schema level rather than defending in the application layer. ProjectService gains LoadCounterclaimChildrenVisible (list visible CCR sub-projects against a parent) and CreateCounterclaim (atomic: project row + creator-as-lead team membership + audit rows on parent AND child). The CCR child is placed as a sibling under the same patent (§4.4), our side flips claimant↔defendant by default with a "Stimmt nicht?" override for the R.49.2.b CCI edge case, and the proceeding type defaults to UPC_REV. Title auto-suggests from the patent ancestor's patent_number when available. Tracker advances 76 → 77. |
||
|
|
85d7dd497c |
feat(t-paliad-173): SmartTimeline Slice 2 backend — projection + anchor + skip + sequence guard
Slice 2 of the SmartTimeline (docs/design-smart-timeline-2026-05-08.md
§6 + §9 + §10) bundled with m/paliad#31's layered requirements:
Migration 076:
- appointments.deadline_rule_id nullable FK to deadline_rules + partial idx
- deadlines.source CHECK widened to include 'anchor' (alongside existing
'manual','fristenrechner','rule','import').
ProjectionService (extended):
- Wires FristenrechnerService + DeadlineRuleService.
- For() now emits Kind="projected" rows for any rule lacking a matching
paliad.deadlines.rule_id / appointments.deadline_rule_id row, with
Status in {predicted | predicted_overdue | court_set}.
- Lookahead cap (default 7, override via ?lookahead=N, max 50): future
predicted rows beyond N are dropped; predicted_overdue + court_set
rows are exempt from the cap (#31 layer 1).
- Dependency annotations DependsOnRuleCode/Date/Name on every row that
carries a DeadlineRuleID, walked from the rule's parent_id chain
(#31 layer 2). Date prefers actuals over projections.
- AnchorOverrides built from completed deadlines (completed_at /
status='completed') + appointments tied via deadline_rule_id.
- triggerDate derives from the proceeding's root rule's anchor when
present, else today() as placeholder.
Anchor write path (POST /api/projects/{id}/timeline/anchor):
- Sequence guard: if rule.parent_id has no anchored actual, return
409 predecessor_missing with the missing rule's code/name DE+EN +
pre-formatted bilingual messages so the frontend can render an
inline error with a "Stattdessen <predecessor> erfassen" link
(#31 layer 3, no confirm-and-write override in v1).
- kind dispatch: rules with event_type IN ('hearing','decision','order')
write paliad.appointments with deadline_rule_id; everything else
writes paliad.deadlines with source='anchor', status='completed',
completed_at=actual_date.
- Idempotent: existing (project_id, rule_id) row PATCHes instead of
inserting (race-safe per design §13).
Skip write path (POST /api/projects/{id}/timeline/skip):
- Writes paliad.project_events with event_type='rule_skipped' +
metadata.rule_code; subsequent reads drop the matching projected
row from the cascade (§6.4).
Handlers expose projection meta via X-Projection-{Has,Total,Shown,Overdue,Lookahead}
headers so the wire shape stays []TimelineEvent (frozen since Slice 1).
|
||
|
|
49c260b888 |
feat(t-paliad-171): migration 075 — project_events.timeline_kind opt-in column
Adds a nullable text column on paliad.project_events so a subset of audit rows can opt into surfacing as SmartTimeline content. Existing rows stay NULL (audit-only); the partial index keeps the lookup tiny because the SmartTimeline read filter is the indexed predicate. Value space (enforced in code in internal/services/projection_service.go): 'milestone' — structural event (counterclaim_filed, ...) 'custom_milestone' — free-text "Eigener Meilenstein" NULL — audit only (default) Design ref: docs/design-smart-timeline-2026-05-08.md §2.2. |
||
|
|
6058d21ce6 |
fix(deadline-rules): pick rule's jurisdiction-aware event_type default
m's 2026-05-08 22:08 dogfood: rule '§ 276 Abs. 1 S. 2 ZPO — Klageerwiderung' (DE) auto-filled to 'Klageerwiderung' label but the chosen event_type was upc_statement_of_defence (UPC). Both render as 'Klageerwiderung' in the UI, but they are different legal events in different jurisdictions. Migration 074 adds a jurisdiction column to paliad.deadline_concept_event_types and swaps the unique-default index from per-concept to per-(concept, jurisdiction). Backfills jurisdiction from each event_type's own column, then re-elects DE / DPMA / EPO defaults where a non-UPC event_type genuinely exists. Idempotent: uses ADD COLUMN IF NOT EXISTS, ON CONFLICT DO UPDATE, partial unique index. DeadlineRuleService.hydrateConceptDefaultEventTypes now JOINs paliad.proceeding_types and matches on (rule.concept, rule.jurisdiction) with EPA→EPO canonicalisation. Rules whose (concept, jurisdiction) has no default stay NULL — silent no-op on the form, better than a wrong jurisdictional default. UPC rules unchanged; DE rules now resolve to de_klageerwiderung when concept = statement-of-defence, else no autofill. Live audit confirms: every active rule now resolves to a same- jurisdiction event_type or no event_type at all. No more cross- jurisdiction matches in the seed. |
||
|
|
4b681792ab |
Merge: t-paliad-165 — Regel ↔ Typ collapse via auto-link on the deadline create form
Two slices on mai/noether/collapse-regel-typ-on: |
||
|
|
0c12644563 |
feat(deadline-rules): expose concept's canonical event_type per rule
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. |
||
|
|
188d8ec9ba |
feat(projects): add projects.our_side column + service plumbing (t-paliad-164 slice 1)
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.
|
||
|
|
6fcf34a3e3 |
feat(determinator/slice-3c): perspective chip + party-tagged cascade narrowing
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.
|
||
|
|
282e0bb237 |
feat(paliadin/migration-070): t-paliad-161 Slice A — schema for agent-suggested write path
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.
|
||
|
|
fdbbc74c15 |
feat(fristenrechner/cascade): more opponent-side proceeding types
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.
|
||
|
|
e2907db760 |
Merge: t-paliad-157 dogfood batch — eye glyph 👀, optional deadlines, Verfahrensablauf collapse
Three commits from mai/feynman/fristenrechner: - |
||
|
|
2d6ea3ee33 |
feat(deadline-rules/is-optional): conditional rules opt-in via save modal
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).
|
||
|
|
9350cd0e87 | Merge remote-tracking branch 'origin/main' into mai/shannon/approval-rework | ||
|
|
3368aa58a6 | chore(migrations): renumber shannon t-160 migrations 064/065 → 066/067 to avoid collision with feynman's t-157 migrations 064 (users.forum_pref) + 065 (event_categories.forums) which landed first | ||
|
|
aec6cf6104 |
refactor(approvals/t-paliad-160 slice3 / M2): drop required_role column
Cleanup of the t-paliad-160 dual-read shim. After slice 1+2 every writer
hits both `required_role` and the new `(requires_approval, min_role)`
columns, and every reader prefers the new ones. M2 (migration 065) drops
the legacy column from `paliad.approval_policies` and rewrites
`paliad.approval_policy_effective()` to a 4-column return shape.
`paliad.approval_requests.required_role` is intentionally untouched —
that's the in-flight policy snapshot at submission time, a separate
concern from the policy authoring grammar.
Go side:
- models.ApprovalPolicy and models.EffectivePolicy lose RequiredRole.
The MinRole pointer is now the only seniority-threshold surface.
- LookupPolicy / GetEffectivePolicyOne / List* / snapshotProjectRows
drop the required_role SELECT projection.
- UpsertProjectPolicySplit / UpsertUnitPolicySplit /
DeleteProjectPolicy / DeleteUnitPolicy / ApplyMatrixToDescendants
drop the required_role write. The audit-log row still uses the
legacy string format ('partner|...|none'); composed via
legacyFromSplit() from the new columns so the audit table layout
keeps working without a parallel migration.
- submit() reads policy.MinRole directly (LookupPolicy guarantees
non-nil when a non-nil policy is returned).
- nullToPtr helper retired (no remaining callers).
Frontend side:
- admin-approval-policies.ts UnitPolicy / EffectivePolicy lose the
legacy required_role optional. The 2-control UI was already on the
split-grammar path.
- deadlines-new.ts + appointments-new.ts form-time hint readers prefer
requires_approval+min_role. They keep a soft-fall back to the
legacy required_role for one cycle in case any cached pre-M2 server
is still serving the old shape — that path is dead-code post-deploy
and can be dropped later.
Test:
- TestApprovalService_PolicyCRUD asserts MinRole instead of
RequiredRole after re-upsert.
Build: bun build OK, go build ./... OK, go test ./... OK.
Deploy ordering: this slice MUST land after slice 2 is merged so the
pre-deploy code paths that still reference required_role have already
been retired.
|
||
|
|
3a41aa9209 |
feat(approvals/t-paliad-160 slice1+2): split policy + 409 handler
m's locked redesign (2026-05-08 16:40): replace `required_role` (with
'none' sentinel) with two columns — `requires_approval boolean` (the
gate) + `min_role text` (the seniority threshold). Cleanly separates
"approval applies at all" from "who's allowed to approve".
M1 phase: additive migration 064 adds the columns, backfills from the
legacy required_role ('none' → false/NULL; else → true/role), and
rewrites paliad.approval_policy_effective() to most-strict-wins:
- requires_approval := bool_or across project + ancestor + unit_default
- min_role := MAX(approval_role_level) among requires_approval=true
The legacy required_role column survives this slice as a dual-read
mirror (resolver returns it too) so any caller that hasn't cut over
keeps working. M2 will drop required_role.
Service layer (approval_service.go): LookupPolicy + GetEffectivePolicyOne
read the new columns; UpsertProjectPolicySplit / UpsertUnitPolicySplit
accept the new shape directly; legacy UpsertProjectPolicy /
UpsertUnitPolicy stay as thin shims that map required_role through
splitFromLegacy(). ApplyMatrixToDescendants writes both columns.
Handler 409 mapping (§B): writeServiceError now consults a shared
mapApprovalError() helper before falling through to the generic 500.
ErrConcurrentPending → HTTP 409 with body
{code: "awaiting_approval", message, request_id?, required_role?}.
PendingApprovalError wraps ErrConcurrentPending with the in-flight
request id + role so the UI knows which request to point a withdraw
button at. ErrNoQualifiedApprover, ErrSelfApproval, ErrNotApprover,
ErrRequestNotPending all mapped consistently. writeApprovalError
now defers to the same helper for shape consistency.
Models: ApprovalPolicy + EffectivePolicy gain RequiresApproval/MinRole
fields. RequiredRole stays as a dual-read mirror until M2.
Tests: TestMapApprovalError_* covers the four 409/403 branches and the
"no match — fall through" case. Existing approval service tests pass
unchanged.
Defers per task spec to follow-up slices:
- A3 (admin UI 2-control flip)
- C+E (badge + withdraw button on detail pages)
- D (/inbox Meine Anfragen visibility fix)
- M2 (drop required_role column)
|
||
|
|
6ef14ddc39 |
feat(fristenrechner/inbox-chip): wire inbox into B1 cascade narrowing
Completes the #15 vision: the inbox chip now narrows the B1 decision tree alongside Pathway A's picker and B2's fine-bucket forum filter. Picking CMS hides DE / EPA / DPMA cascade entries; picking beA / Posteingang hides UPC / EPA / DPMA entries. Neutral nodes (top-level branches, Mündliche Verhandlung sub-states, court-generic events like Ladung / Kostenfestsetzung) stay visible from every inbox setting so the user can always reach the cross-jurisdictional middle of the tree. Migration 065 adds paliad.event_categories.forums (text[]) with a CHECK on {upc, de, epa, dpma}, a partial GIN index, and a two-step backfill: 1. Regex on slug for nodes that carry the forum token explicitly. Token-bounded by ^/./- so .dpma doesn't trip the de pattern. 2. Explicit slug list for stragglers (BGH / BPatG / Versäumnisurteil / Hinweisbeschluss are DE-only; r116-eingaben is EPA-only). NULL stays neutral. Migration applied to live Supabase; tracker at v65. Backend: EventCategoryNode JSON gains an optional `forums` array; EventCategoryService.Tree SELECT includes the column and threads it through to the response. Frontend: new module-level currentInboxChannel mirrors the chip state so renderB1Cascade can ask "which forum is active?" without re-deriving from the URL on every step. inboxFilterAllowsForums(forums) gates each child node — neutral arrays (undefined / empty) always pass; tagged arrays must include the active forum. applyInboxFilter re-renders the cascade so chip clicks reflow B1 in place. Pathway A picker filter and B2 fine-bucket sync remain orthogonal — same chip, three filters. Refs m/paliad#15 (B1 follow-up). |
||
|
|
06bd276a9c |
feat(users/forum-pref): persisted Fristenrechner inbox-channel column
Adds paliad.users.forum_pref so /tools/fristenrechner can pre-narrow the proceeding picker to the user's typical inbox channel without re-asking on every visit. The new column threads through the User model, the userColumns SELECT, and UpdateProfileInput so the existing PATCH /api/me handler accepts it without a new endpoint. Allowed values mirror the channel chips m named in t-paliad-157: - cms → UPC - bea → national-DE - posteingang → national-DE (slower channel, same forums) NULL means "no preference, picker shows everything"; URL ?inbox= overrides per-visit (frontend lands in the next commit). The CHECK constraint enforces the 3-value enum at the DB layer; isValidForumPref mirrors it in the service so callers see a typed error instead of a raw pq violation. Empty string in the PATCH body clears the preference, consistent with the EscalationContactID convention. Migration 064 applied to the live Supabase pool; tracker bumped to v64 so the boot-time runner skips re-applying. Refs m/paliad#15. |
||
|
|
7c751617e5 |
fix(fristenrechner/cascade): add UPC R.320 path under "Frist verpasst"
The "Frist verpasst" cascade covered DE PatG/ZPO, EPA Art.122 and DPMA
Wiedereinsetzung paths but had no UPC option, even though UPC R.320 RoP
grants re-establishment of rights with the same shape (2 months from
removal of the obstacle, 12-month outer limit).
Migration 063 adds:
- trigger_event id 207 "Wegfall des Hindernisses (UPC R.320)" tied to
the existing wiedereinsetzung concept, so the concept card picks
up a UPC pill alongside DE / EPA / DPMA.
- event_categories leaf frist-verpasst.upc at sort_order 50 so UPC
reads first under "Frist verpasst" (national + EPA siblings stay
at 100/200/300/400).
- event_category_concepts junction linking the new leaf to the
wiedereinsetzung concept; NULL proceeding_type_code mirrors the
sibling pattern (cross-cutting trigger pills bypass the forum
filter by design — per-leaf narrowing is part of the IA-reframe
issue #16).
Migration applied to live Supabase; matview refreshed; tracker bumped
to v63 so the boot-time runner skips re-applying.
Refs m/paliad#14 section C.
|
||
|
|
e92c56b5f8 |
feat(t-paliad-154) commit 1.5: extend migration 062 with policy_audit_log
Q8 of locked design: policy CRUD audits to /admin/audit-log only, NOT to per-project /verlauf. The 4 existing audit sources (project_events, caldav_sync_log, reminder_log, partner_unit_events) don't fit cleanly: project_events would surface on /verlauf (rejected by Q8); partner_unit_events constrains event_type and requires unit_name + a non-null partner_unit_id which doesn't fit project-scoped policy changes. Added paliad.policy_audit_log as a fifth audit source — admin-only, scoped either to a project or a partner unit, snapshots scope_name so post-cascade rows still render. RLS: select for any authenticated user (route gate is the actual control); write for global_admin only. AuditService.ListEntries will union this source in commit 2 of this PR. Validated insert/select live in BEGIN ... ROLLBACK. |
||
|
|
f7908f03ad |
feat(t-paliad-154) commit 1/5: migration 062 — approval_policies unit-defaults + 'none' sentinel + resolver + seed
Schema:
- ALTER paliad.approval_policies: project_id nullable, ADD partner_unit_id
uuid REFERENCES paliad.partner_units(id) ON DELETE CASCADE.
- XOR check: exactly one of (project_id, partner_unit_id) is set.
- Replace UNIQUE composite with two partial unique indexes (one per scope).
- Extend required_role CHECK with 'none' sentinel.
- approval_role_level('none') already returns 0 via existing ELSE branch
in 059_profession_vs_responsibility.up.sql:218 — no function update.
Resolver paliad.approval_policy_effective(project, entity_type, lifecycle):
- Step 1: project-specific row wins outright (any value, including 'none').
- Step 2: MAX(approval_role_level) across ancestor rows on project's path
+ unit-default rows for partner units attached to project. Tied levels
break alphabetically ('ancestor' beats 'unit_default') for stable
attribution.
- Step 3: zero rows (no candidates) — caller treats as 'no policy applies'.
Returns (required_role, source, source_id) — source ∈ {project, ancestor,
unit_default}; source_id is project_id or partner_unit_id depending.
Seed:
- 8 rows × every existing partner_unit (currently 11): deadline+appointment
× create/update/delete = associate; complete = none.
- ON CONFLICT (partner_unit_id, entity_type, lifecycle_event)
WHERE partner_unit_id IS NOT NULL DO NOTHING — idempotent on re-run
(verified live: 11 units → 88 seed rows, second run is no-op).
- Safe on a DB with 0 partner_units (SELECT returns no rows).
Down migration: reverse-order. Coerces 'none' rows to 'associate' before
restoring CHECK so rollback works without data loss. Drops seeded unit
rows; preserves project rows that pre-date 062.
Validated end-to-end against the live DB inside BEGIN ... ROLLBACK; the
existing project policy (deadline:create=partner) is preserved by the
DO NOTHING clause and the partial-index scope.
Design: docs/design-approval-policy-ui-2026-05-07.md §3.1.
No RAISE EXCEPTION. No bare CSS tokens (no CSS in this commit).
|
||
|
|
4e1d311a9c |
feat(t-paliad-149) PR2 step 1/3: backend — migration 061 + CardLayoutService + CardsPreview
Migration 061 (paliad.user_card_layouts): per-user named card layouts.
- Partial unique index on (user_id) WHERE is_default=true keeps "at most
one default per user" honest at the DB level.
- UNIQUE (user_id, name) so the layout dropdown can use names as stable
labels.
- RLS owner-only (mirrors paliad.user_views from t-144).
LayoutSpec (internal/services/layout_spec.go): structured JSON validator
with KnownFactKeys registry (11 fact keys: title-row, type-chip, status-
chip, client-matter, parent-path, deadline-counts, next-events, recent-
verlauf, team-chips, reference, last-activity-at). Validator enforces:
- title-row must be the first VISIBLE fact (always-on, structural)
- no duplicate keys
- count ∈ [1, 5] only on next-events / recent-verlauf
- density ∈ {compact, roomy} (CardDensity, distinct from t-144's
ListDensity which only ranges over comfortable/compact)
- grid_columns ∈ {auto, 2, 3, 4}
DefaultLayoutSpec returns the m-locked rich content set per design §5b.4
(9 facts, roomy density, auto grid, leaf-ish projects only).
CardLayoutService: CRUD with auto-seed (GetDefault creates "Standard"
on first call) + tx-flip-default (setting is_default=true on B clears
A in the same transaction) + ErrUserCardLayoutDefaultGate (deleting
the active default returns 409). isPgUniqueViolation maps the partial
unique index conflict to ErrUserCardLayoutNameTaken.
ProjectService.CardsPreview: per-project event rollups for the Cards view.
4 source SQLs with ROW_NUMBER() OVER PARTITION BY project_id (top 3 each
for upcoming deadlines, upcoming appointments, recent project_events) +
team-chips JOIN. Single round-trip per source, visibility-gated. Returns
map[uuid.UUID]*ProjectCardPreview with last_activity_at computed across
all sources for the orchestrator's card-grid sort.
Handlers: 5 /api/user-card-layouts/* endpoints (GET list, POST create,
PATCH update, DELETE, POST set-default) + GET /api/projects/cards-preview
(narrowable via ?ids=<csv>).
Wired in handlers.go (Services struct + dbServices struct) and
cmd/server/main.go. ErrUserCardLayoutNameTaken / NotFound / DefaultGate
mapped to 409 / 404 / 409 respectively.
Tests:
- layout_spec_test.go (8 cases, pure-Go): valid default, empty rejection,
title-row-first invariant, hidden leading allowed, dup-key rejection,
unknown-key rejection, count-bounds + count-on-wrong-key, density/grid
enum, ParseLayoutSpec round-trip.
- card_layout_service_test.go (6 cases, live-DB): GetDefault auto-seeds
+ idempotent, first Create auto-becomes default, SetDefault clears
prior, Delete refuses active default, Delete non-default works,
duplicate name rejected, Update round-trips layout JSON.
go build / vet / test (short) clean.
Design: docs/design-projects-page-2026-05-07.md §5b.3, §5b.5, §8.2.
|
||
|
|
8412328dec |
feat(t-paliad-149) PR1 step 1/3: backend — migration 060 + PinService + BuildTreeWithOptions
Migration 060 (paliad.user_pinned_projects): per-user, RLS owner-only, ON
DELETE CASCADE on both FKs.
PinService (Pin / Unpin / IsPinned / PinnedSet / ListPinned): visibility-
gates pin (can't pin what you can't see) but not unpin (so users can clean
up after losing access). PinnedSet returns a map for O(1) lookups during
tree stitching.
ProjectService.BuildTreeWithOptions extends BuildTree with chip-driven
filtering. New ProjectTreeNode fields are additive (Pinned,
InheritedVisibility, OpenDeadlinesSubtree, OverdueDeadlinesSubtree,
MatchKind) so the old BuildTree(ctx, userID) call still works for legacy
callers. New options:
Scope: All / Mine / Pinned (Mine + Pinned both expand to path-closure
with InheritedVisibility flag on greyed ancestors)
StatusIn / TypeIn: chip-narrowing whitelists
HasOpenDeadlines: per-node or subtree-aggregated, depending on
IncludeSubtreeCounts
SearchTerm: case-fold contains on title/reference/clientmatter, then
prune to {matches ∪ ancestors ∪ descendants} with match_kind tagged
IncludeSubtreeCounts: post-order DFS sums, O(N)
GET /api/projects/tree gains query params: scope, status, type,
has_open_deadlines, q, subtree_counts. Zero query string preserves
legacy behaviour.
POST/DELETE /api/projects/{id}/pin and GET /api/user-pinned-projects
wired. Service registered in cmd/server/main.go and dbServices.
build + vet clean.
Design: docs/design-projects-page-2026-05-07.md §4.7, §8.1, §8.3.
|
||
|
|
efaa7787af | Merge remote-tracking branch 'origin/main' into mai/kepler/inventor-profession-vs | ||
|
|
c6cdd2c855 | fix(t-paliad-148): renumber migration 057→059 (collision with fritz t-147 email_broadcasts already on main; noether t-146 paliadin_poc landed at 058) | ||
|
|
7b66c4d035 |
feat(t-paliad-146): Paliadin PoC — tmux-Claude in-app AI buddy
Phase 0 of the Paliadin design (docs/design-paliadin-2026-05-07.md
§0.5). m-only laptop scope, gated behind PALIADIN_ENABLED=false on
prod. Lifts the goldi/mVoice tmux-Claude pattern (mVoice/server.py:
250-380) into a Go service: long-lived `claude` pane in a tmux
session, prompts in via `tmux send-keys -l`, responses out via a
per-turn file (/tmp/paliadin/{turn_id}.txt) the system prompt
instructs Claude to write.
What landed
-----------
- migration 058_paliadin_poc — paliad.paliadin_turns audit table
(full prompt + response stored at PoC scope; redaction returns
at production v1 per design §3.3). RLS: user sees own,
global_admin sees all.
- internal/services/paliadin.go — the orchestrator. ensurePane()
finds-or-creates the tagged tmux window, sendToPane sends the
framed [PALIADIN:turn_id] envelope, pollForResponse reads the
per-turn file, splitTrailer parses the [paliadin-meta] block
Claude appends to every reply (used_tools, rows_seen,
classifier_tag).
- internal/services/paliadin_prompt.go — the system prompt sent
once to a fresh Claude pane. Defines the response protocol
(Write-to-file + meta trailer), the action-chip marker syntax,
the visibility-gate rule (paliad.can_see_project required in
every project-scoped query), and 9 SQL recipes covering m's
paliad data + cross-schema youpc case-law lookup.
- internal/handlers/paliadin.go — POST /api/paliadin/turn kicks
off the work in a goroutine and returns an SSE URL; GET
/api/paliadin/stream/{id} relays per-turn channel events
(meta/content/end/error/ping) to EventSource. Routes register
ONLY when PaliadinService is wired — paliadinSvc nil → no
handlers exist, prod surface is clean.
- /admin/paliadin dashboard — global_admin-only. Shows total
turns, last-7-days, median/p90 duration, tool-use rate (the
load-bearing §0.5.7 metric), abandon rate, classifier
histogram, daily sparkline, top prompts, recent turn log.
Powered by PaliadinService.Stats() + ListRecentTurns().
- frontend: paliadin.tsx + client/paliadin.ts (chat panel with
starter prompts, EventSource consumer, typewriter render of
one-shot content blob, citation-chip parser, "Stop" + "New
conversation" buttons, localStorage history); admin-paliadin
pair (read-only stats dashboard).
- Sidebar: Paliadin entry under Übersicht (ICON_SPARKLE);
Paliadin Monitor under Admin.
- 36 i18n keys (DE+EN), CSS for chat panel + dashboard.
- main.go: PaliadinService wires only on PALIADIN_ENABLED=true,
with PALIADIN_TMUX_SESSION + PALIADIN_RESPONSE_DIR overrides.
Logs visibly so the operator can confirm at boot.
- CLAUDE.md: ANTHROPIC_API_KEY row updated (PoC doesn't need it
— Claude CLI uses m's subscription; key reserved for future
production-v1). New rows for the three PALIADIN_* env vars.
Tests
-----
- 7 unit tests on the trailer parser, chip counter, token approx,
and tmux-input sanitiser. All pass. The trailer parser is
load-bearing for monitoring; an unobserved parser bug = silent
dashboard rot.
What's NOT in v1 (stays deferred)
---------------------------------
- The Anthropic API client (production v1, gated on PoC success
per §0.5.7).
- BYO-AI / OpenAI adapter.
- Per-user rate limiting.
- Multi-replica SSE bus.
- Mascot / avatar SVG.
- Persistent threads (history is browser localStorage only).
How to use locally
------------------
$ export PALIADIN_ENABLED=true
$ ./paliad
# browse /paliadin → type a question → answers stream back
# /admin/paliadin shows the monitoring dashboard
Migration: 058 (skips fritz's t-147 on 057). Safe on prod
because PALIADIN_ENABLED defaults to false; the table is created
but no routes touch it until the env var flips.
|
||
|
|
ab2530ff44 |
feat(t-paliad-148) commit 1/6: migration 057 — schema + backfill + user_project_authority_level
Adds paliad.users.profession (firm-wide career tier) and paliad.project_teams.responsibility
(per-project responsibility, default 'member'). Backfills both from the legacy
project_teams.role column — highest-tier-per-user for profession, single-row map
for responsibility (lead→lead, observer→observer, local_counsel/expert→external,
others→member).
Updates paliad.approval_role_level to recognise 'partner' as the new ceiling
(replaces 'lead' as the firm-tier ceiling), keeping 'lead' at level 5 as a
deprecated-shadow row until follow-up migration 058 retires project_teams.role.
Updates paliad.approval_role_from_unit_role: lead → partner.
Creates paliad.user_project_authority_level(user_id, project_id) — the
tuple-with-gate ladder. Returns profession_level if responsibility ∈ {lead,member}
else 0; max with derived authority via partner-unit attachments where
derive_grants_authority=true.
Updates approval_policies.required_role + approval_requests.required_role CHECK
constraints (drop 'lead', add 'partner'); backfills any existing rows.
Rewrites project_partner_units write RLS policy to read pt.responsibility='lead'
instead of pt.role='lead'.
Live-DB BEGIN/ROLLBACK dry-run verified: 2 users get profession='partner'
(matthias.siebels, tester@hlc.de — the only users currently on project_teams),
45 users get profession=NULL (admin fills via /admin/team).
project_teams.role kept as deprecated shadow column. Drop in follow-up migration 058.
|
||
|
|
52ee319fd8 |
feat(t-paliad-147): bulk team email — send to filtered selection from /team page
Implements issue #7. Adds an "E-Mail an Auswahl" button on /team that sends personalised emails to a filter-narrowed subset of the team. Each recipient gets their own envelope (per-recipient privacy, no shared To: list); From stays on the SMTP infrastructure address with Reply-To set to the human sender so replies route correctly without forging DKIM/SPF. Backend - Migration 057: paliad.email_broadcasts (subject, body, sender_id, template_key, recipient_filter jsonb, recipient_user_ids uuid[], send_report jsonb, sent_at). RLS: senders read own rows, global_admin reads all; inserts must self-attribute. No CHECK-constraint extension to partner_unit_events — broadcasts get their own table per the lock. - BroadcastService (internal/services/broadcast_service.go): validates subject/body/recipient cap (100), enforces project_lead-OR-global_admin, persists audit row, dispatches via 5-deep goroutine pool with 15s per-send timeout. Send report (sent/failed counts + per-recipient errors) is captured back into email_broadcasts.send_report. - markdown.go: minimal Markdown→safe HTML renderer (paragraphs, **bold**, *italic*, `code`, [text](url), bullet lists). Inputs are HTML-escaped first; only whitelisted tags re-emitted. Script tags and javascript: URLs can't slip through. - Placeholder substitution: {{name}}, {{first_name}}, {{role_on_project}} (whitespace tolerated). Unknown {{...}} tokens pass through unchanged. - mail_service.go: buildMIMEWithReplyTo helper layers a Reply-To header on top of the existing multipart/alternative envelope. - TeamService.ListMembershipsIndex: visibility-gated user→project_ids index. Powers the /team project multi-select filter without N round trips per project. - Handlers: POST /api/team/broadcast (gateOnboarded; service enforces authority), GET /api/team/memberships, GET /api/admin/broadcasts (list), GET /api/admin/broadcasts/{id} (detail), GET /admin/broadcasts (page). /admin/broadcasts is gateOnboarded (not adminGate) so leads can see their own sends; the service applies the per-row visibility filter. Frontend - /team gains a project multi-select chip dropdown (visible projects loaded from /api/projects, intersected against the memberships index) alongside the existing office and role filters. - "E-Mail an Auswahl (N)" button appears only when canBroadcast() is true (global_admin always; non-admin needs lead-ship on selected projects, or at least one project when no filter is set). Server still re-checks per send. - Compose modal (broadcast.ts): subject + body textarea + optional template dropdown (loads existing email templates and strips Go-template directives) + recipient preview (first 5 + expand) + send. Hard-blocks empty subject/body and N=0. Shows per-send report on success. - /admin/broadcasts viewer: read-only list with click-row-to-expand detail (subject, body, recipient list, send_report counts). Tests - broadcast_service_test.go: placeholder substitution table-driven, Markdown safe-render incl. XSS guards (<script>, javascript: URLs), validation cases (empty subject/body, recipient cap, invalid email), signature rendering DE/EN. - broadcast_service_live_test.go: end-to-end Send + List + Get + visibility rules (lead can send on own project, member cannot, admin sees all, member can't read lead's row). Skips when TEST_DATABASE_URL is unset. i18n: 60 new keys × 2 langs (broadcast modal labels, error messages, recipient summary, /admin/broadcasts viewer, common.close/loading/forbidden/ load_error). |
||
|
|
b516201110 |
feat(t-paliad-144 A1): backend substrate + Custom Views API
Phase A1 of the data-display-model rethink (m/paliad#5). Backend-only; no user-visible change in A1. A2 (frontend) lands separately. What's new: - Migration 056: paliad.user_views table with RLS scoped to caller (user_views_owner_all on auth.uid()=user_id). Composite UNIQUE (user_id, slug). No is_system flag — system defaults stay code- resident per Q8 lock-in. - internal/services/filter_spec.go (+test): structured FilterSpec with Sources / Scope / Time / Predicates. Server-side validator rejects unknown sources, duplicate sources, conflicting scope modes, horizon=all without explicit projects (Q26 clamp), and every per-source enum (deadline.status, appointment_types, project_event kinds, approval_request status / viewer_role). - internal/services/render_spec.go (+test): RenderSpec with three shapes (list / cards / calendar — Q4 lock-in 2026-05-07). Per-shape config kept separately so flipping shapes preserves tweaks. Validator over column / sort / density / group_by / default_view enums. - internal/services/system_views.go (+test): code-resident SystemView definitions for dashboard / agenda / events / inbox / inbox-mine. Reserved-slug list (Q23) prevents user-views from colliding with top-level URLs. Case-folded matching. - internal/services/view_service.go: extends EventService with RunSpec — runs a FilterSpec across all four substrate sources (deadline + appointment + project_event + approval_request) and merges into []ViewRow sorted by event_date. ViewRow is a discriminated projection (kind + common header + per-source Detail json.RawMessage). Q17 fail-open attribution: returns inaccessible_project_ids for explicit-scope queries where the caller can't see some IDs. - internal/services/user_view_service.go (+test): CRUD on paliad.user_views — Create (server-assigns sort_order MAX+1 in tx), GetBySlug, GetByID, Update (partial), Delete, Touch (last_used_at), MostRecent. Reserved-slug + slug-format validators on every write. - internal/handlers/views.go: nine HTTP handlers wiring the endpoints (GET/POST/PATCH/DELETE /api/user-views/..., POST /api/user-views/{id}/touch, POST /api/views/run, POST /api/views/{slug}/run, GET /api/views/system). - main.go + handlers.go + projects.go: wire UserViewService into the bundle; conditional route registration when both UserView + Event services are present. Pure-Go tests (no DB): 32 cases pass — filter spec validators, render spec validators, system view registry, reserved slugs. Live-DB tests (skip when TEST_DATABASE_URL unset): 12 cases covering create / list / get / uniqueness / update / delete / touch / most-recent / reserved-slug / bad-slug / empty-name / invalid-spec. Coexists with t-139 (in-flight on noether's other branch) and t-138 (shipped) without coordination commits — RunSpec uses the existing visibility predicate that t-139's migration 055 will extend with derivation. Approval-request source delegates to ApprovalService.ListPendingForApprover / ListSubmittedByUser (both already extended for derived_peer authority in t-139 Phase 3). Files: 15 changed, 3134 insertions. Build clean. Tests green. |
||
|
|
a61c1490e3 |
feat(t-paliad-139): Phase 3 — derived_peer authority extension to t-138 approval gate
Wires DerivationService.EffectiveProjectRole into the t-paliad-138
approval ladder so partner-unit-derived members with derive_grants_authority=true
can act as approvers (per design §4.2). When they sign off, the audit row
records decision_kind='derived_peer' — a third value alongside the existing
'peer' and 'admin_override' — so the chronology discloses the derivation
chain.
Schema (migration 055 update)
-----------------------------
- paliad.approval_requests.decision_kind CHECK extended to accept
'derived_peer'. Down migration restores the t-138 two-value CHECK.
Live SQL dry-run confirmed the new value is accepted.
Service layer
-------------
- approval_levels.go: new constant DecisionKindDerivedPeer.
- approval_service.go (4 sites widened with the derivation EXISTS branch):
1. canApprove — third resolution step after global_admin + direct/
ancestor team membership: matches partner-unit-derived members
on path with derive_grants_authority=true and a unit_role whose
approval_role_from_unit_role mapping meets the threshold.
Returns DecisionKindDerivedPeer when this branch is the one that
passed.
2. hasQualifiedApprover (the deadlock-check at submit time) —
widened so a project with no direct approvers but an authority-
granting unit attachment is still submittable.
3. ListPendingForApprover (the /inbox query) — third UNION ALL
branch so derived authority sees their queue.
4. PendingCountForUser (the bell-badge query) — same widening so
derived authority sees the count tick.
All four queries reuse paliad.approval_role_from_unit_role(text) added
by Phase 2 of migration 055.
Frontend
--------
- 2 i18n keys (DE+EN): approvals.decision_kind.derived_peer →
"Genehmigt durch abgeleitetes Mitglied (Partner Unit)" / "Approved by
derived member (Partner Unit)". Verlauf rendering of the third
decision_kind value works through the existing translateEvent /
decision_kind switch with no other change. 1606 keys total.
Strict-default unchanged
------------------------
Derived members are visibility-only by default. Authority requires the
project lead/admin to explicitly flip derive_grants_authority=true on the
project_partner_units row (UI on /projects/{id} Team tab, Phase 2). This
preserves the m-locked Q12 stance.
Phase 3 closes the t-paliad-139 implementation. m's bug closes (Phase 1),
the derivation schema is in place (Phase 2), and approval authority
flows through the new ladder (Phase 3).
|
||
|
|
544bb63684 |
feat(t-paliad-139): Phase 2 — partner-unit derivation schema + Team-tab subsections
Migration 055 adds the structural pieces the issue's PA-derivation premise
needed (the design-§1.3 verify-before-trust check found all three were
missing today):
- paliad.partner_unit_members.unit_role text DEFAULT 'attorney'
CHECK ('lead'|'attorney'|'senior_pa'|'pa'|'paralegal') — per-unit role
distinction so derivation can target specific tiers without re-
introducing a firm-wide rank column. The same human can be 'attorney'
in one unit and 'lead' in another.
- paliad.project_partner_units junction (project_id, partner_unit_id,
derive_unit_roles[] DEFAULT {pa,senior_pa}, derive_grants_authority bool
DEFAULT false, attached_at, attached_by) with composite PK and RLS
(read = can_see_project; write = global_admin OR project lead).
- paliad.approval_role_from_unit_role(text) helper used by Phase 3 when
derived authority is consulted by the t-138 ladder.
- paliad.can_see_project extended with one EXISTS branch — derivation
walks the path: a user is visible on P if any (ancestor of P) is
attached to a unit they are a member of with a matching unit_role.
No RAISE EXCEPTION (Maria's build constraint). Day-1 deploy = zero
behaviour change because every existing unit member defaults to
unit_role='attorney' and the default derive_unit_roles is {pa,senior_pa},
so until both diverge no derivation happens.
Backend services
----------------
- DerivationService (new, internal/services/derivation_service.go):
AttachUnitToProject, DetachUnitFromProject, ListAttachedUnits,
ListDerivedMembers (path-walking dedupe by closest attachment),
ListDescendantStaffed (descendant-direct rows excluding ancestor-
already-staffed), EffectiveProjectRole (returns role + source ∈
{direct, ancestor, derived} for the t-138 approval gate in Phase 3).
- PartnerUnitService extensions:
PartnerUnitMemberDetail gains UnitRole (db:"unit_role"). Constants
UnitRoleLead/Attorney/SeniorPA/PA/Paralegal + isValidUnitRole.
SetMemberRole(callerID, unitID, userID, role) with admin gate, prior-
role read in tx, audit emit 'member_role_changed'. ListMembers and
ListWithMembers SELECT projection now includes pum.unit_role.
Handlers
--------
- GET /api/projects/{id}/partner-units → ListAttachedUnits
- POST /api/projects/{id}/partner-units → AttachUnitToProject
- DELETE /api/projects/{id}/partner-units/{unit_id} → DetachUnitFromProject
- GET /api/projects/{id}/team/derived → ListDerivedMembers
- GET /api/projects/{id}/team/from-descendants → ListDescendantStaffed
- PATCH /api/partner-units/{id}/members/{user_id}/role → SetMemberRole
- Services bundle gains Derivation; cmd/server/main.go wires it.
Frontend (Team-tab on /projects/{id})
-------------------------------------
Three new subsections rendered after the existing direct+ancestor table:
- "Aus Unterprojekten" — descendant-direct rows with attribution arrow.
- "Abgeleitet (Partner Unit)" — derived rows with [Sicht] / [Sicht & 4-
Augen] badge per the m-locked honesty rule (§3.5).
- "Partner Units" — attached-unit list with attach/detach controls
(lead/admin only) and a form picker for derive_unit_roles +
derive_grants_authority.
Each subsection is hidden when its data is empty (Partner Units block
also surfaces for managers when empty so they can attach).
Loaders + state in projects-detail.ts; renderTeam orchestrates all
four subsections; renderAttachedUnits owns the unit list + detach
handlers; initAttachUnitForm wires the picker + checkbox role-set.
canManagePartnerUnits gates the attach UI on global_admin OR direct
'lead' on the current project.
i18n keys (DE+EN, ~30 new) under projects.team.section.*,
projects.team.derived.*, projects.team.units.*, unit_role.*. Codegen now
emits 1605 keys (was 1494).
CSS additions: .entity-section-heading (subsection h3),
.derived-badge / .derived-badge--authority, .form-checkbox.
Phase 3 (approval extension to honour derived_peer decision_kind) stacks
on top — gates on EffectiveProjectRole returning ('role','derived') being
wired into the t-138 canApprove + inbox SQL.
|
||
|
|
b3401ec8ac |
feat(t-paliad-138): migration 054 — dual-control approvals schema
Schema-only commit (1 of 8) for the 4-Augen-Prüfung workflow per docs/design-approvals-2026-05-06.md. No Go code reads these yet — paliad behaves identically until commit 2 wires ApprovalService into the mutation paths. Migration 054 adds: 1. `senior_pa` to paliad.project_teams.role CHECK. Drops both the English `project_teams_role_check` and the German-legacy `projekt_teams_role_check` (live-DB constraint name carried over from migration 018's pre-rename era). 2. `paliad.approval_role_level(text) RETURNS int IMMUTABLE` — strict ladder helper: lead(5) > of_counsel(4) > associate(3) > senior_pa(2) > pa(1) > [local_counsel/expert/observer = 0 = ineligible]. Mirrors the upcoming Go `levelOf()`. 3. `paliad.approval_policies` (project_id, entity_type, lifecycle_event, required_role) — UNIQUE composite key gives at most 8 rows per project. RLS: SELECT via can_see_project; INSERT/UPDATE/DELETE only for global_admin (defence-in-depth; service-role pool bypasses RLS, so the actual gate is service-layer). 4. `paliad.approval_requests` — operational pending workflow. pre_image jsonb captures revert state; payload echoes the diff; required_role snapshots the policy at request time. CHECK `decided_by != requested_by` is the second layer of self-approval block. RLS = same can_see_project predicate as deadlines / appointments — anyone with project visibility sees pending requests. 5. `approval_status` (default 'approved'), `pending_request_id`, `approved_by`, `approved_at` columns on both deadlines and appointments. `appointments.completed_at` (new) lands the appointment:complete lifecycle event. 6. Backfill: every existing deadline + appointment row marked approval_status='legacy'. Per Q11, no retroactive approval; the next mutation on a legacy row that hits an active policy follows the normal flow. Live-DB dry-run verified end-to-end: 20 deadlines + 5 appointments backfill, both new tables instantiate cleanly, helper function returns correct levels, self-approval CHECK fires on invalid INSERT, valid pending insert succeeds. |
||
|
|
a9d3695719 |
feat(t-paliad-122): migration 053 — courts entity + countries lookup + regime split
Adds paliad.countries (13 ISO-3166 codes), paliad.courts (41 entries seeded from internal/handlers/courts.go), and the country/regime split on paliad.holidays. The 33 t-paliad-121 UPC vacation rows previously stored as country='UPC' migrate cleanly to country=NULL + regime='UPC' — 'UPC' is a supranational regime, not an ISO country, and the new shape lets a UPC LD München (country='DE', regime='UPC') pull both DE federal holidays and UPC vacation entries while a UPC LD Paris (country='FR', regime='UPC') pulls FR + UPC. Holidays now FK-protected against typo'd country codes. |
||
|
|
d22ace1019 |
feat(t-paliad-136): Phase C — RoP-rigorous tree taxonomy revision
Migration 052 fixes six concept↔leaf mismaps in the v3 seed and adds
three proactive entry leaves under spaetere-schriftsaetze.
1. cms-eingang.gericht.hinweisbeschluss — drop the response-to-
preliminary-opinion | DE_INF row. DE_INF (LG) has no
Hinweisbeschluss; the concept lives only in DE_NULL via PatG §83.
2. cms-eingang.gegenseite.upc-inf.klageschrift — drop the notice-of-
defence-intention | UPC_INF row. UPC has no such rule in the corpus;
R.23 reaction is captured by statement-of-defence directly.
3. UPC R.221 cost-appeal sequence (m's Q5): three leaves now surface
BOTH application-for-leave-to-appeal | UPC_COST_APPEAL (sort 100,
R.221.1, 15 days) AND notice-of-appeal | UPC_APP (sort 200,
conditional on leave granted, R.220.1). Replaces the wrong notice-of-
appeal | UPC_COST_APPEAL row that was silently dropping pills.
4. ich-moechte-einreichen.berufung.upc-coa-orders — replace the buggy
application-for-leave-to-appeal | UPC_APP_ORDERS (no rule for that
combo) with request-for-discretionary-review | UPC_APP_ORDERS
(R.220.3).
5. cms-eingang.gericht.anordnung — narrow request-for-discretionary-
review NULL → UPC_APP_ORDERS. R.220.3 review applies specifically
to the Anordnungen / 15-day track.
6a. reply-to-cross-appeal coverage: add UPC_APP rows under upc-{inf,
rev}.berufungsschrift so the reply leaf is reachable when the
opponent files an Anschlussberufung.
6b. New leaves under ich-moechte-einreichen.spaetere-schriftsaetze for
proactive entry: r116-eingaben (EPA R.116 final submissions),
anschlussberufung-upc (R.237), reply-to-cross-appeal-upc (R.238).
NO `RAISE EXCEPTION` coverage gate (m's Q7) — last night's outage was
caused by exactly that pattern in migration 049. Replaced with a Go-
side test in event_category_coverage_test.go that asserts every
category='submission' concept is reachable from at least one leaf
(except the prosecution-only exempt list: filing, request-for-
examination, approval-and-translation). Skipped without
TEST_DATABASE_URL; CI gates on it.
bescheid-mit-frist mapping deferred per m's Q4. Will land separately.
Migration verified via supabase MCP dry-run + ROLLBACK on the live
youpc DB; end-state matches design §3.2-§3.4.
|
||
|
|
63eb5bde6f |
feat(t-paliad-134): pill ordering + name standardisation + chip dedup
Five m's-bookmark fixes on top of the B1 surface change:
1. Sort proceeding pills inside concept cards by real-world frequency.
New paliad.proceeding_types.display_order column (m's spec values:
UPC_INF=10, DE_INF=20, UPC_REV=30, ..., UPC_PI=920, ...). Default
999 for unmapped legacy codes. Search service surfaces it through
the deadline_search matview (rebuilt to add the column) and uses
it as primary key in pillSortKey, replacing the jurisdiction-rank.
2. Name standardisation: -klage → -verfahren on the proceeding-types
that describe a multi-step process. Specifically:
UPC_REV Nichtigkeitsklage → Nichtigkeitsverfahren
UPC_APP Berufung → Berufungsverfahren
DE_INF Verletzungsklage (LG) → Verletzungsverfahren (LG)
DE_INF_OLG, DE_NULL_BGH, DPMA_OPP, DPMA_BPATG_BESCHWERDE,
UPC_COST_APPEAL, UPC_APP_ORDERS, DPMA_BGH_RB, DE_INF_BGH —
same -verfahren standardisation.
3. legal_source for rev.defence × UPC_REV: was NULL, leaking the
internal local_code 'rev.defence' to the UI. Set to UPC.RoP.49.1
(Defence to Application for Revocation, R.49.1).
4. Frontend renderPill no longer falls back to rule_local_code when
legal_source is missing — the source span just collapses, so no
internal slug ever shows up as a "citation".
5. Quick-pick chips refactored to a slug-based array (QUICK_CHIPS) in
fristenrechner.tsx, single source of truth for both fork-shortcut
and B2-search-bar rows. Each chip carries data-chip-name-de /
data-chip-name-en; relabelChips() rewrites visible text per active
language. Dropped the duplicate "Statement of Defence" chip (same
concept as "Klageerwiderung"). Each chip now maps to one concept
slug — Klageerwiderung→statement-of-defence, Berufung→notice-of-
appeal, Einspruch→opposition, Replik→reply-to-defence,
Beschwerde→nichtzulassungsbeschwerde, Schadensbemessung→
application-for-determination-of-damages, Wiedereinsetzung→
wiedereinsetzung.
Migration 051 uses RAISE WARNING (not EXCEPTION) on coverage gates
per the 049 outage lesson — partial-migration recovery beats whole-
transaction failure. Matview rebuild stays inside the transaction;
RefreshSearchView() on next boot is a cheap no-op.
|
||
|
|
ff36528148 |
fix(t-paliad-133): add reply-to-cross-appeal to coverage exempt list
Migration 049 went dirty in prod because the coverage gate at the end (DO $coverage$) raised on 'reply-to-cross-appeal' — it's defined as a submission concept but no leaf in the decision-tree seed maps to it. reply-to-cross-appeal is a downstream-of-cross-appeal concept, only reachable after the user has already entered the cross-appeal Pathway B branch via 'response-to-appeal'. Adding a dedicated leaf would be useful UX (file a follow-up), but for now exempting it from the coverage gate matches the established 'pure-administrative' exemption pattern used for filing / request-for-examination / approval-and-translation. Manual recovery: set tracker version=48 dirty=false on prod (schema from 048 was already applied via supabase MCP). Dokploy redeploy will now run 049 + 050 cleanly and reach version=50. Refs: t-paliad-133 prod outage 11:15-11:30 Tue 05.05.2026 |
||
|
|
f40b652d01 |
Reapply "Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs)"
This reverts commit
|
||
|
|
5bd17de732 |
Revert "Merge: t-paliad-133 — Fristenrechner v3 (Pathway A/B fork + B1 decision tree + B2 forum filter + retire legacy tabs)"
This reverts commit |
||
|
|
4d820892e8 |
feat(t-paliad-133): Phase A — event taxonomy schema + seed + bilateral flag
Three migrations land the data layer for the Fristenrechner v3 decision tree (Pathway B / B1) plus the bilateral-rule flag for the new party- perspective selector. All purely additive — no breaking changes to the v2 (t-paliad-131) corpus. Migration 048 — schema: - paliad.event_categories: recursive taxonomy tree (parent_id self-FK, unique slug as materialised dot-path, step_question_de/en on internal nodes, is_leaf bool, optional emoji icon). - paliad.event_category_concepts: many-to-many junction (leaf → deadline_concepts) with optional proceeding_type_code narrowing. UNIQUE NULLS NOT DISTINCT prevents duplicate (leaf, concept, NULL) rows (PG 15+). - paliad.deadline_rules.is_bilateral bool: when true AND primary_party='both', the rule mirrors into both party columns of the v3 columns view; otherwise 'both' resolves single-side via the perspective selector. Migration 049 — seed taxonomy: 6 root buckets (cms-eingang, muendl-verhandlung, beschluss-entscheidung, frist-verpasst, ich-moechte-einreichen, sonstiges) with 70+ leaves and 115+ junction rows. Tree depth reaches 4 today (cms-eingang › gericht › endentscheidung › <leaf>) but the schema supports unlimited depth per design lock §10 Q2. Coverage gate at the end raises if any category='submission' concept is unreachable from a leaf, except the 3 pure-administrative slugs (filing, request-for-examination, approval-and-translation) that live on Pathway A only. Migration 050 — bilateral backfill: Tags exactly 4 genuinely-bilateral rules: - de_null.stellungnahme (Stellungnahme zum Hinweisbeschluss, PatG §83.2) - epa_opp.r79_further (Stellungnahme weiterer Beteiligter) - epa_opp.r116, epa_app.r116 (Eingaben vor mündl. Verhandlung) All other primary_party='both' rules (Berufungsfristen, Anschlussberufung, …) are role-swap appeals that resolve via the perspective selector at render time. Schema dry-run validated end-to-end against Supabase PG 15.8. Design ref: docs/plans/unified-fristenrechner-v3.md §4.1 + §10 Q12. |
||
|
|
b45278b060 |
feat(t-paliad-131): Phase C — search backend (matview + service + handler)
Closes the search half of the unified Fristenrechner. Phase D (concept-card
UI on /tools/fristenrechner) follows in a subsequent shift.
Migration 047:
- Seed the missing `wiedereinsetzung` concept and re-point the four
Wiedereinsetzung trigger_events (200..203) at it. PR-7 referenced
the slug `re-establishment-of-rights` but never seeded the concept,
so the four cross-cutting triggers were dropping out of any concept-
JOINing query. Per m's slug rule (Q1: shared cross-cutting concepts
use DE slug because German term dominates HLC vocabulary).
- Create paliad.deadline_search materialised view: UNION ALL of
(deadline_rules joined to deadline_concepts) and (trigger_events
joined to deadline_concepts via slug). Trigram GIN indexes on
legal_source / concept_name_de / concept_name_en / rule_name_de /
rule_name_en / rule_code; gin (concept_aliases) for array
containment; UNIQUE INDEX on a synthetic row_key so refresh can
run CONCURRENTLY.
Refresh strategy: data only mutates via migration files at server
startup, so no AFTER triggers and no pg_cron — main.go calls
services.RefreshSearchView right after db.ApplyMigrations. CONCURRENTLY
keeps reads online and stays well under 100 ms at < 1k rows.
Service `internal/services/deadline_search_service.go`:
- Two-query pipeline per request: (1) rank concept_ids by
GREATEST(similarity()) across name / aliases / legal_source / rule_code
plus a 0.2 alias-hit boost; (2) load all matview rows for the top-N
concepts and assemble per-pill JSON.
- normalizeQuery strips legal-prefix noise (`§`, `Art.`, `Section`,
`Rule `) so users typing `§ 82` find DE.PatG.82.1 even though the
structured legal_source column doesn't carry the prefix.
- FormatLegalSourceDisplay renders structured codes back to the
pleading form HLC users expect:
UPC.RoP.23.1 → "UPC RoP R.23(1)"
DE.PatG.82.1 → "PatG §82(1)"
EU.EPÜ.108 → "EPÜ Art.108"
EU.EPC-R.79.1 → "EPC R.79(1)"
EU.RPBA.12.1.c → "RPBA Art.12(1)(c)"
- Drill URLs route per kind: rule pills → ?proc=…&focus=…, trigger
pills → ?mode=event&triggerId=…
Handler `GET /api/tools/fristenrechner/search?q=&party=&proc=&source=&limit=`:
- Returns the JSON shape from design §6.1 (cards-with-pills).
- 503 with friendly DE message when DATABASE_URL is unset, mirroring
the other Fristenrechner endpoints.
- Empty q returns an empty cards array (browse surface is Phase D).
Tests:
- Pure-Go: TestFormatLegalSourceDisplay (12 cases across all known
prefixes) + TestNormalizeQuery (8 cases).
- Integration (skipped without TEST_DATABASE_URL): golden table
pinning the design's binding queries — Klageerwiderung returns the
statement-of-defence card with UPC.RoP.23.1, DE.ZPO.276.1,
DE.PatG.82.1, EU.EPC-R.79.1, DE.PatG.59.3 pills; "RoP 23" returns
the same card; "§ 82" → normalized "82" → BPatG hit; Wiedereinsetzung
returns one card with exactly 4 trigger pills (ids 200..203);
party / source filters narrow as expected; limit cap honoured.
- SQL semantics validated against live data via supabase MCP using a
CTE-inlined matview definition with the slug fix simulated; results
match the golden table.
Per design doc `docs/plans/unified-fristenrechner.md` §4.6 (matview
shape) + §6 (search ranking + API).
|
||
|
|
53d5e5306c |
feat(t-paliad-131): Phase B6 — cross-cutting concepts (Wiedereinsetzung × 4 + Versäumnis + Schriftsatznachreichung + Weiterbehandlung)
PR-7 of the Unified Fristenrechner. Final Phase B migration. Closes
all named cross-procedural deadline gaps in the design.
These concepts fire across many proceedings (any patent application,
any civil case, any opposition, any appeal) and don't naturally belong
to one proceeding-tree timeline. Modelled per design §5.2.4 + §5.3 as
event-trigger-only entries: the user picks the trigger ("the moment
the obstacle was removed", "the date the Versäumnisurteil was served")
and the calculator returns the deadline.
Migration 046 adds 7 trigger_events (ids 200–206, paliad-native space
above the youpc-imported 1–114 range so future resync stays clean) and
7 corresponding event_deadlines + 3 new concepts.
WIEDEREINSETZUNG IN 4 LEGAL CONTEXTS (one shared concept slug
re-establishment-of-rights, seeded in PR-1):
- PatG §123(2): trigger 200, 2 months / max 1 year
- ZPO §234(1): trigger 201, **2 WEEKS** / max 1 year
← critical distinction; the 2-weeks-not-months ZPO
case is the most-confused detail of DE
Wiedereinsetzung. notes_de explicitly capitalises
"WOCHEN" so the user reads it before computing.
- EPC Art.122 + R.136(1): trigger 202, 2 months / max 12 months
- DPMA via PatG §123: trigger 203, 2 months / max 1 year
OTHER CROSS-CUTTING:
- Versäumnisurteil-Einspruch (ZPO §339): trigger 204, 2 weeks
Notfrist — keine Verlängerung möglich.
- Schriftsatznachreichung (ZPO §296a): trigger 205, 3 weeks
(court-set typical; placeholder the user can adjust via
click-to-edit if the court actually set a different period)
- Weiterbehandlung (Art.121 EPÜ + R.135): trigger 206, 2 months
Distinct from Wiedereinsetzung — niedrigere Gebühr, applies
BEFORE final loss of rights.
Three new concepts (slug naming per design §4.4):
- versaeumnisurteil-einspruch (DE-only procedure → DE slug)
- schriftsatznachreichung (DE-only → DE slug)
- weiterbehandlung (EPC-native + DE term dominates HLC vocab → DE slug)
Live-verified all 7 trigger_events on paliad.de (tester@hlc.de) via
the existing /tools/fristenrechner "Was kommt nach…" mode:
trigger 200 → 2026-07-06 (2mo PatG, weekend-shift)
trigger 201 → 2026-05-18 (2 WEEKS ZPO — the critical case)
trigger 204 → 2026-05-18 (2 weeks ZPO §339)
trigger 205 → 2026-05-26 (3 weeks ZPO §296a)
trigger 206 → 2026-07-06 (2mo EPC weiterbehandlung)
Out of scope (no calculator-relevant deadlines, would just be search
clutter): Mahnverfahren-Widerspruch (ZPO §345), Validierungsfristen
national (Art. 65 EPÜ → varies per state), Teilanmeldung (R.36 EPC →
"until end of pending parent" is anchor-on-revocation-of-grant).
Phase B is now complete. Phase C (search backend) + Phase D (concept-
card UI) follow per design.
|
||
|
|
706afb617f |
feat(t-paliad-131): Phase B5 — EPA gap-fill (R.79.2/3, R.116, R.106) + EPA_OPP/APP anchor fix
PR-6 of the Unified Fristenrechner. Fills the EPA-side coverage gaps
named in the design + repairs three pre-existing EPA bugs surfaced
during this work.
Migration 045:
PRE-EXISTING BUG FIXES
1. EPA anchor convention bug. epa_opp.grant and epa_app.entsch were
seeded with party='court' + event_type='decision' → calculator's
isCourtDeterminedRule(r) returned true → those anchor rows
rendered as IsCourtSet (no date), propagating IsCourtSet to every
downstream rule that chained off them. Result on prod: EPA_OPP
showed "court-set" for Einspruchsfrist / Erwiderung / Entscheidung
instead of computed dates; ONLY the trailing beschwerde + begr
rendered dates (and only by accident, because they had parent_id=
NULL and computed off triggerDate directly).
Fix: changed both anchors to party='both' + event_type='filing' so
they render as IsRootEvent. Matches the convention I established
for DE_INF_OLG / DE_INF_BGH / DE_NULL_BGH / DPMA_BPATG_BESCHWERDE /
DPMA_BGH_RB anchors in PR-3/4/5.
2. EPA_OPP appeal-phase parent bug. epa_opp.beschwerde +
beschwerde_begr had parent_id=NULL → were computing 2mo and 4mo
from the GRANT date instead of from the OPPOSITION DECISION date.
Re-parented both on epa_opp.entsch. They now correctly render as
IsCourtSet placeholders (because entsch is court-set) until the
user enters the real decision date via the Phase A click-to-edit
affordance.
3. EPA_APP.erwidg modelling bug. Was parent_id=NULL + duration=0 +
party=both + event=filing → IsRootEvent → emitted the trigger date
as "Erwiderung". Now properly modelled per Art. 12(1)(c) RPBA 2020:
parent=epa_app.begr, duration=4 months, name="Beschwerdeerwiderung",
legal_source=EU.RPBA.12.1.c, response-to-appeal concept.
NEW COVERAGE (per design §5.3)
EPA_OPP gains 2 rules:
- epa_opp.r79_further: Stellungnahme weiterer Beteiligter
(R.79(2)/(3) EPC) — court-set, parent=erwidg
- epa_opp.r116: Eingaben vor mündl. Verhandlung
(R.116(1) EPC) — court-set, parent=entsch (so it surfaces in the
opposition phase but stays IsCourtSet until oral hearing date is
entered via override)
EPA_APP gains 2 rules:
- epa_app.r116: Eingaben vor mündl. Verhandlung
(R.116(1) EPC + Art. 13 RPBA) — court-set, parent=oral
- epa_app.r106: Antrag auf Überprüfung
(Art. 112a EPÜ) — 2 months from service of decision, parent=
entsch2 (the BoA decision)
Three new EN-slug concepts (UPC/EPC-native): r79-further-stellungnahme,
r116-final-submissions, petition-for-review.
Live-verified on paliad.de:
EPA_OPP trigger 2026-05-04 → grant IsRootEvent / Einspruchsfrist
2027-02-04 (9mo) / Erwiderung 2027-06-04 (4mo from frist) /
r79_further 2027-06-04 (filed-with-erwidg) / Entscheidung +
Beschwerde + Begründung + r116 IsCourtSet (waiting for entsch).
EPA_APP trigger 2026-05-04 → entsch IsRootEvent / Beschwerde
2026-07-06 (2mo, weekend-shift) / Begründung 2026-09-04 (4mo from
entsch) / Beschwerdeerwiderung 2027-01-04 (4mo from Begründung
per RPBA 12.1.c) / r116 IsCourtSet (parent=oral) / r106 IsCourtSet
(parent=entsch2, will compute 2mo from BoA decision once entered).
Out of scope (deferred to PR-7 cross-cutting): Wiedereinsetzung
(Art. 122 EPÜ + R.136 EPC), Weiterbehandlung (Art. 121 EPÜ + R.135 EPC),
Validierungsfrist national (Art. 65 EPÜ).
|
||
|
|
25076142f4 |
feat(t-paliad-131): Phase B4 — DPMA proceeding chain
PR-5 of the Unified Fristenrechner. Three new proceeding types
covering the DPMA → BPatG → BGH opposition / appeal chain. Closes the
DPMA gap m named — paliad has had zero DPMA-specific timelines until
now (DPMA-granted patents in Nichtigkeit went to DE_NULL but the DPMA
opposition + Beschwerde + Rechtsbeschwerde chain had no home).
Migration 044 adds:
- DPMA_OPP (Einspruch DPMA, sort=310): 4 rules. Anchor "Veröffentlichung
der Erteilung" + Einspruchsfrist (PatG §59.1, 9mo) + Erwiderung
Patentinhaber (PatG §59.3, court-set ~4mo, party=defendant) +
DPMA-Entscheidung (court).
- DPMA_BPATG_BESCHWERDE (Beschwerde BPatG, sort=320): 5 rules. Anchor
"Zustellung DPMA-Entscheidung" + Beschwerde (PatG §73.2, 1mo) +
Beschwerdebegründung (PatG §75.1, 1mo from filing, extension on
request) + mündliche Verhandlung + BPatG-Entscheidung.
- DPMA_BGH_RB (Rechtsbeschwerde BGH, sort=330): 4 rules. Anchor
"Zustellung BPatG-Entscheidung" + Rechtsbeschwerde (PatG §100.1, 1mo)
+ Begründung (PatG §102 i.V.m. ZPO §551, 1mo from filing) +
BGH-Entscheidung.
Naming note: head's PR brief listed the third type as
"DPMA_BPATG_NICHTIGKEIT" but Nichtigkeitsklage is filed directly at
BPatG (already covered by DE_NULL — never chained off DPMA). The
natural BGH endpoint of the DPMA chain is the Rechtsbeschwerde per
§§ 100/102 PatG. Using DPMA_BGH_RB; trivially renamable if head
intended a different shape.
Two new DE-only concepts: rechtsbeschwerde (BGH legal appeal — DE-
specific procedure, no UPC/EPC equivalent), rechtsbeschwerde-
begruendung. Other rules reuse shared concepts (publication,
opposition, statement-of-defence, notice-of-appeal, statement-of-
grounds-of-appeal, oral-hearing, decision).
Frontend: new DPMA tile group in /tools/fristenrechner with 3 tiles,
positioned after the EPA group. 5 new i18n keys (DE+EN: deadlines.dpma
group label + 3 tile names + tile labels for 3 procs).
Live-verified all 3 trees on paliad.de (tester@hlc.de):
DPMA_OPP trigger 2026-05-04 → Einspruch 2027-02-04 (9mo) /
Erwiderung 2027-06-04 (4mo from Einspruch).
DPMA_BPATG_BESCHWERDE trigger 2026-05-04 → Beschwerde 2026-06-04
(1mo) / Begründung 2026-07-06 (1mo from Beschwerde, weekend-shift).
DPMA_BGH_RB trigger 2026-05-04 → Rechtsbeschwerde 2026-06-04 /
Begründung 2026-07-06.
|
||
|
|
e3b093d9a2 |
feat(t-paliad-131): Phase B3 cont — DE instance-split proceeding types
PR-4 of the Unified Fristenrechner. Three new proceeding types so the
user can pick "I'm at OLG defending a Berufung" or "I'm at BGH on the
Nichtigkeitsberufung" and get the per-instance timeline directly,
rather than chaining off DE_INF / DE_NULL trailing rows.
Migration 043 adds:
- DE_INF_OLG (Berufung OLG, sort_order=210): 7 rules. Anchor
"Zustellung LG-Urteil" + Berufungsschrift (ZPO §517, 1mo) +
Berufungsbegründung (ZPO §520(2), 2mo, anchored on Urteil not on
notice) + Berufungserwiderung (ZPO §521(2), court-set 1mo typ.) +
Anschlussberufung (ZPO §524(2), filed-with-erwiderung) +
mündl. Verhandlung + OLG-Urteil.
- DE_INF_BGH (Revision/NZB BGH, sort_order=220): 8 rules. Anchor
"Zustellung OLG-Urteil" + parallel NZB (§544.1, 1mo) /
NZB-Begründung (§544.4, 2mo) / Revisionsfrist (§548, 1mo) /
Revisionsbegründung (§551.2, 2mo) — all four from the
OLG-Urteil-Datum since they're alternatives. Plus
Revisionserwiderung (§554, 1mo court-set) + mündl. + BGH-Urteil.
- DE_NULL_BGH (Berufung BGH gegen Nichtigkeit, sort_order=230): 6
rules. Anchor "Zustellung BPatG-Urteil" + Berufungsschrift
(PatG §110.1, 1mo) + Berufungsbegründung (PatG §111.1, 3mo) +
Berufungserwiderung (PatG §111.3 → ZPO §521.2, 2mo court-set typ.)
+ mündl. + BGH-Urteil.
Anchor convention: synthetic 0-duration root rule "Zustellung [prev-
instance] Urteil" with party='both' + event_type='filing' so it
renders as IsRootEvent (= the trigger date). Per design, this is the
honest model — the user enters the actual previous-instance Urteil
date, no fabricated inter-stage gap.
Four new DE-only concepts (per slug rule: DE for German-only
procedures): nichtzulassungsbeschwerde, nichtzulassungsbeschwerde-
begruendung, revisionsfrist, revisionsbegruendung. Other rules reuse
the existing shared concepts (notice-of-appeal, statement-of-grounds-
of-appeal, response-to-appeal, cross-appeal, oral-hearing, decision).
Frontend: 3 new tiles in DE_TYPES + 8 new i18n keys (DE+EN). Tiles
appear between DE_INF and DE_NULL per sort_order grouping.
Out of scope (kept in DE_INF / DE_NULL trees during transition until
Phase D Full Appeal Chain ships): the existing trailing rows
de_inf.berufung / de_inf.beruf_begr / de_null.berufung /
de_null.beruf_begr stay live so users picking those trees still see
the appeal-period entry. Phase D will gate the visibility.
Live-verified all 3 trees on paliad.de:
DE_INF_OLG trigger 2026-05-04 → Berufung 2026-06-04 (1mo) /
Begründung 2026-07-06 (2mo from Urteil, weekend-shift) /
Erwiderung 2026-08-06 (1mo from Begründung) / Anschluss
2026-08-06 (filed-with-erwiderung).
DE_INF_BGH trigger 2026-05-04 → NZB 2026-06-04 (1mo) /
NZB-Begr 2026-07-06 / Revision 2026-06-04 / RevBegr 2026-07-06
(parallel options) / RevErw 2026-08-06.
DE_NULL_BGH trigger 2026-05-04 → Berufung 2026-06-04 / Begr
2026-08-04 (3mo per PatG §111.1 = the now-fixed seed) / Erwidg
2026-10-05 (2mo from Begr, weekend-shift).
|
||
|
|
24e22511ec |
feat(t-paliad-131): Phase B3 — DE expansion (PatG §111 fix + BPatG Hinweisbeschluss + ZPO Anzeige)
PR-3 of the Unified Fristenrechner. Three concerns bundled in migration
042 since they touch only DE_INF / DE_NULL trees and ship together
without coverage interactions:
1. PatG §111(1) bug fix. Current paliad seed had de_null.beruf_begr at
1 month. Current text of §111(1) BGBl. 2022: "Die Frist zur
Begründung der Berufung beträgt drei Monate. Sie beginnt mit der
Zustellung des in vollständiger Form abgefassten Urteils, spätestens
mit Ablauf von fünf Monaten nach der Verkündung." Bumped to 3 months
+ deadline_notes documenting the 5-month outer cap.
2. DE_NULL Hinweisbeschluss cycle (PatG §83). 4 new rules added between
Klageerwiderung and Mündliche Verhandlung:
- de_null.replik_klaeger (Replik, 2mo typical court-set, R.83.2)
- de_null.hinweisbeschluss (court order, R.83.1) — IsCourtSet
- de_null.stellungnahme (response, parent=hinweisbeschluss, R.83.2)
— IsCourtSet via parent propagation
- de_null.duplik (Rejoinder, 1mo typical court-set, R.83.2)
The court-set typical durations match the existing DE_INF replik/
duplik pattern — a placeholder date the user can override via the
Phase A click-to-edit affordance once the court actually sets it.
3. DE_INF Anzeige der Verteidigungsbereitschaft (ZPO §276(1) Satz 1).
New rule de_inf.anzeige, 2 weeks from Klage, defendant. Was the
biggest gap in the LG-civil cycle.
Three new concepts: preliminary-opinion (court order, sort 65),
response-to-preliminary-opinion (submission, sort 39),
notice-of-defence-intention (submission, sort 19). All seeded with
DE+EN aliases for search.
DE_INF + DE_NULL sequence_orders renumbered to leave gaps so future
inserts (B6 cross-cutting Wiedereinsetzung, B4-style instance-split)
can interleave without re-renumbering.
Live-verified on paliad.de (tester@hlc.de):
- DE_INF trigger 2026-05-04 → Anzeige 2026-05-18 (2w), Erwiderung
2026-06-15 (6w), backbone unchanged.
- DE_NULL trigger 2026-05-04 → Klageerwiderung 2026-07-06 (2mo),
Replik 2026-09-07 (2mo from Erwiderung, weekend-shift), Duplik
2026-10-07 (1mo from Replik), Hinweisbeschluss + Stellungnahme
IsCourtSet, Berufungsbegründung 2026-09-04 (3mo, was 1mo).
Out of scope (deferred to B6): cross-cutting Wiedereinsetzung,
Versäumnisurteil-Einspruch (only fires on default), Schriftsatz-
nachreichung. Out of scope (deferred to PR-4): new instance-split
proceeding types DE_INF_OLG / DE_INF_BGH / DE_NULL_BGH.
|
||
|
|
cc68ab2873 |
feat(t-paliad-131): Phase B1 — UPC counterclaim cross-flows
Closes m's primary complaint: today's `with_ccr` flag on UPC_INF only
swaps the Replik / Duplik durations. Per UPC RoP R.29 the with-CCR flow
ALSO adds 5–7 new submissions across the claimant / defendant exchange.
Same gap on UPC_REV: Application to amend (R.49.2.a → R.55 = R.32 m.m.)
and Counterclaim for infringement (R.49.2.b → R.50, R.56 cycle) were
entirely missing.
UPC_INF gets a nested `with_amend` flag under `with_ccr` (R.30 amend
is only available with a CCR). UPC_REV gets two parallel independent
flags `with_amend` + `with_cci`; both can be on. Citations verified
against data.laws_contents (youpcdb, UPCRoP).
Migration 041 (waved INSERTs because each subsequent rule references
the prior wave's parent_id):
- Wave 0: 11 new concept rows (counterclaim-for-revocation,
defence-to-counterclaim-for-revocation, defence-to-application-to-amend,
reply-to-defence-to-counterclaim-for-revocation,
reply-to-defence-to-application-to-amend,
rejoinder-on-reply-to-defence-to-ccr, rejoinder-on-reply-to-amend,
counterclaim-for-infringement, defence-to-counterclaim-for-infringement,
reply-to-defence-to-counterclaim-for-infringement,
rejoinder-on-counterclaim-for-infringement). counterclaim-for-revocation
also seeded for the search bar even though its rule lives implicitly
in inf.sod (the with_ccr flag captures it).
- UPC_INF + UPC_REV sequence_orders renumbered to leave gaps (10/20/30…)
so new cross-flow rows interleave chronologically with the backbone.
- 7 new UPC_INF rules: inf.def_to_ccr (R.29.a), inf.app_to_amend (R.30.1),
inf.def_to_amend (R.32.1), inf.reply_def_ccr (R.29.d),
inf.reply_def_amd (R.32.3), inf.rejoin_reply_ccr (R.29.e),
inf.rejoin_amd (R.32.3).
- 8 new UPC_REV rules: rev.app_to_amend (R.49.2.a), rev.def_to_amend
(R.43.3), rev.reply_def_amd (R.32.3 m.m.), rev.rejoin_amd (R.32.3 m.m.),
rev.cc_inf (R.49.2.b), rev.def_cci (R.56.1), rev.reply_def_cci (R.56.3),
rev.rejoin_cci (R.56.4).
Calculator (services/fristenrechner.go):
- Zero-duration rules now split into 4 buckets, not 2:
1. parent=nil + non-court → IsRootEvent (existing)
2. parent=nil + court → IsCourtSet (existing, e.g. inf.oral when stand-alone)
3. parent set + court → IsCourtSet (existing, waypoints)
4. parent set + non-court → "filed-with-parent" — inherit parent's
date. NEW. Used by rev.app_to_amend / rev.cc_inf which per
R.49(2) are filed AS PART OF the Defence to revocation.
- AnchorOverrides on a zero-duration rule short-circuits to the user's
date, propagating downstream as before.
Frontend:
- New checkboxes inf-amend-flag (UPC_INF, nested under ccr-flag),
rev-amend-flag, rev-cci-flag (UPC_REV). Visibility per proceeding
type; inf-amend disabled until ccr is on (R.30 dependency).
- Three new i18n keys (DE+EN). Small CSS for nested-checkbox indent
and disabled-state colour.
Live-verified via curl on paliad.de against tester@hlc.de:
UPC_INF + with_ccr+with_amend, trigger 2026-05-04 → all 7 new rules
render at correct dates (R.29.a 2mo, R.30.1 2mo, R.32.1 2mo from
app_to_amend, R.29.d 2mo from def_to_ccr, R.32.3 1mo, R.29.e 1mo,
R.32.3 1mo).
UPC_REV + with_amend+with_cci → rev.app_to_amend / rev.cc_inf show
rev.defence's date (filed-with-parent), R.43.3 2mo / R.56.1 2mo /
R.32.3 + R.56.3 1mo / R.32.3 + R.56.4 1mo all line up.
|
||
|
|
78966ec098 |
feat(t-paliad-131): Phase A — concept layer + AnchorOverrides + click-to-edit dates
PR-1 of the Unified Fristenrechner. Purely additive: new search-grouping layer + per-rule date override capability. No coverage changes yet (those land in PR-2 = Phase B1 UPC counterclaim cross-flows). Migrations: - 037: paliad.deadline_concepts (id, slug, name_de/en, aliases text[], party, category, sort_order). Trigram + GIN indexes for the search bar. - 038: deadline_rules.concept_id (uuid FK), legal_source (text); event_deadlines.legal_source; trigger_events.concept_id (text slug, soft-link — youpc imports keep their bigint PK). - 039: deadline_rules.condition_flag text → text[] (USING ARRAY[old]). Semantic: rule renders iff every element is in CalcOptions.Flags. Single-element arrays preserve the legacy with_ccr swap exactly. - 040: seed 30 concept rows + backfill all 74 fristenrechner deadline_rules with concept_id; backfill legal_source from existing rule_code (e.g. 'RoP.023' → 'UPC.RoP.23.1', '§ 276 ZPO' → 'DE.ZPO.276.1', 'Art. 108 EPÜ' → 'EU.EPÜ.108', 'R. 79(1) EPÜ' → 'EU.EPC-R.79.1'). Calculator (services/fristenrechner.go): - ConditionFlag is now pq.StringArray (matches text[] schema). New allFlagsSet() helper gates rule rendering; rules with multi-element flags require ALL of them set (prep for Phase B1 with_amend ∧ with_cci). - CalcOptions.AnchorOverrides map[string]string (rule_code → YYYY-MM-DD). The tree-walk consults overrideDates[parent.code] before reading the computed-date map; lets a downstream rule re-anchor on a user-set date. - IsCourtSet rows that get an override stop being placeholder and emit the user's date as a real anchor (so downstream cost_app etc. compute off it). New IsOverridden flag in UIDeadline so the UI can highlight user-edited rows. - LegalSource surfaced on UIDeadline for future search-card display. UI (frontend/src/client/fristenrechner.ts + global.css + i18n): - Each timeline / column rule date is click-to-edit. Click → inline date input → blur or Enter → POST with anchorOverrides → re-render. Empty value clears the override. Escape cancels. Root-event rows (the trigger anchor) stay non-editable — that's the trigger-date input. - Override map cleared on proceeding switch / reset; persists across trigger-date / flag toggle changes within the same proceeding. - New CSS: subtle hover underline on .frist-date-edit; lime border on .timeline-date--overridden + .frist-date-edit-input. - New i18n key deadlines.date.edit.hint (DE + EN). Handler (handlers/fristenrechner.go): - POST body gains optional anchorOverrides map<string,string>; passed through to CalcOptions. Tests: - TestAllFlagsSet covers single-flag legacy semantic, two-flag AND semantic, empty-required unconditional, extra-flags-no-effect. - Existing TestIsCourtDeterminedRule unchanged. Phase A ships standalone — Phase B1 (UPC counterclaim cross-flows) and Phase C/D (search backend + concept-card UI) follow. |
||
|
|
4e1213fbd1 |
fix(t-paliad-116): event_deadlines i18n follow-up — title_de backfill + notes_en
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:
A) title_de empty for all 70 rows. The DTO already falls back to title
silently, so DE locale rendered English titles ("Statement of Defence",
"Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
terminology that matches the trigger_events.name_de translations from
mig 033, so the picker label and the deadline row read the same.
B) notes column carries English text on rows 50, 52, 70 (DE-named column
was DE-only in spec, but seeds slipped EN strings through). Mig 036
adds a parallel notes_en column following the t-112 mig 032 pattern,
copies the existing English into notes_en, and replaces notes with
proper DE for those three rows.
Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
proceeding-tree timeline branch already in fristenrechner.ts)
|
||
|
|
53f7eae665 |
fix(t-paliad-111): renumber colliding migration 032→034 — production was down
Two migrations both named 032 collided when t-111 and t-112 merged in parallel — 032_deadline_notes_en (t-112, already applied to the DB and tracker bumped to v33) vs. 032_deadlines_rule_code (t-111). The Go migration runner refuses to init the driver when two files share a prefix, so paliad.de was 404 across all routes (container in restart loop with `migration failed: ... duplicate migration file: 032_deadlines_rule_code.down.sql`). Renumbering t-111's pair to 034 (033 was used by t-112's trigger_events_de backfill). |