8412328dec4fd43cd819157e861f0c36826b81ec
188 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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) | ||
|
|
8d714dd95e |
fix(t-paliad-146): gate Paliadin to owner email in code, drop PALIADIN_ENABLED
m's call (2026-05-07 21:52): "remove the export variable, that is bad form. It should be connected only to my account." The PALIADIN_ENABLED env var was a deploy-time toggle: easy to mis-flip, splits prod/dev behaviour, and reads as "could be turned on for anyone." Replaced with a per-request gate in code: services.PaliadinOwnerEmail = "matthias.siebels@hoganlovells.com" handlers/paliadin.go now gates every entry point through requirePaliadinOwner, which looks up paliad.users.email by the caller's UUID and returns 404 (not 403 — pretend the route doesn't exist) for anyone else. Routes register unconditionally; the gate is in the code, not the deploy. main.go wires PaliadinService whenever DATABASE_URL is set and logs the owner identity at boot. CLAUDE.md drops the PALIADIN_ENABLED row and gains an explanatory note about the in-code gate. Sidebar entries (Paliadin under Übersicht; Paliadin Monitor under Admin) now render with display:none, revealed by sidebar.ts after /api/me confirms the caller's email matches PALIADIN_OWNER_EMAIL — same fail-closed pattern the Admin group already uses. Side-effect for ops: paliad.de production now serves the routes too, but only to m, and only successfully if the host has tmux + claude in PATH (which Dokploy doesn't). m hitting /paliadin from prod gets a "tmux unavailable" — clear failure mode, not a security concern. One new test (TestPaliadinOwnerEmail_IsLowercaseStable) keeps the constant aligned with migration 023's seed so a future rename of m's account doesn't silently strand the gate. All existing tests pass. |
||
|
|
0b4de1c645 |
feat(t-paliad-148) commit 6/6: deprecation notes + grep sweep
Mark the legacy Role* constants in project_service.go as DEPRECATED.
They stay defined for one release because team_service.go still writes
the deprecated shadow column via legacyRoleFromResponsibility; follow-up
migration 058 (t-paliad-149) retires both the column and the constants.
Final grep sweep clean: no live-code call sites remaining for
project_teams.role outside of:
- the deprecated legacyRoleFromResponsibility mapper (intentional)
- team_service.go RETURNING + SELECT (reads the shadow column for
the JSON .role field still surfaced for the deprecation window)
- migrations 018/023/054/055 (historical, not modified)
Test suite green across all packages: auth, branding, calc, changelog,
handlers, offices, services. Frontend bun build clean (1723 i18n keys).
|
||
|
|
9184e9b0ef |
feat(t-paliad-148) commit 4/6: reminder + deadline + derivation cleanup — pt.role → pt.responsibility
reminder_service.go: BuildDigest audience predicate switches the
"project lead anywhere on the path" branch from `pt.role = 'lead'` to
`pt.responsibility = 'lead'`. Two SQL sites + comment updated.
deadline_service.go: assertCanAdminProject (Reopen permission) switches
from `pt.role IN ('admin','lead')` to `pt.responsibility = 'lead'`.
The legacy 'admin' was already dead since t-paliad-051 — never present
in project_teams.role to begin with — so this also drops a slow leak.
Doc comments + error message updated.
derivation_service.go: ListDescendantStaffed SELECTs both `pt.role` and
`pt.responsibility`, returns the new column to the team-tab "from
descendants" subsection (so the firm-tier badge + responsibility pill
both render). ORDER BY switches to responsibility.
Build + vet clean. Pure-Go tests pass.
|
||
|
|
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.
|
||
|
|
e6937d232e |
feat(t-paliad-148) commit 3/6: TeamService + UserService + Models + Handlers — write profession + responsibility
Models:
- ProjectTeamMember.Responsibility (new) + .Role (kept as deprecated shadow). JSON exposes both during the deprecation window.
- ProjectTeamMemberWithUser.UserProfession — populated by reads so the team-tab UI can render the firm-tier badge.
- User.Profession (*string) — structured firm-tier driving the approval ladder. Distinct from JobTitle (display) and GlobalRole (tool admin).
TeamService:
- AddMember signature kept as (callerID, projectID, userID, responsibility) — third arg renamed conceptually. Accepts the new responsibility enum and writes both legacy `role` (via legacyRoleFromResponsibility helper) and `responsibility` to keep the deprecated shadow consistent.
- ListDirectMembers + ListEffectiveMembers SELECT both `pt.role`, `pt.responsibility`, and `u.profession`. ORDER BY switches from pt.role to pt.responsibility.
- legacy isValidRole removed (unused after switch to IsValidResponsibility).
UserService:
- CreateUserInput + AdminCreateInput + AdminUpdateInput accept Profession. Self-service onboarding defaults to 'associate' when empty. AdminCreate likewise. AdminUpdate empty-string clears to NULL (external collaborator). Invalid values rejected with ErrInvalidInput.
- INSERT statements write the new column on both Create paths.
ProjectService.Create:
- Auto-add-creator INSERT writes responsibility='lead' alongside legacy role='lead'.
Handlers:
- POST /api/projects/{id}/team accepts `responsibility` (preferred) and falls back to legacy `role` for one release while frontend migrates.
Build + vet clean. Pure-Go tests pass.
|
||
|
|
6506864730 |
feat(t-paliad-148) commit 2/6: ApprovalService + DerivationService — tuple-with-gate ladder
Rewires the 4 SQL ladder sites in approval_service.go (canApprove,
hasQualifiedApprover, ListPendingForApprover, PendingCountForUser) to read
the new tuple: project_teams.responsibility ∈ {lead, member} AND
users.profession at or above the threshold. observer/external rows close
the gate even if the user's profession would otherwise qualify — that's
the project-level call.
approval_levels.go renamed levelOf → professionLevel and added
responsibilityOpensGate helper. New constants: ProfessionPartner /
ProfessionOfCounsel / … and ResponsibilityLead / ResponsibilityMember /
ResponsibilityObserver / ResponsibilityExternal. New validators
IsValidProfession + IsValidResponsibility. RoleSeniorPA kept as legacy
alias for the one remaining call site that hasn't migrated yet.
CRITICAL trap pinned by TestProfessionLevel_NilIsZero: NULL profession
returns 0, never silently defaults to associate. External collaborators
must stay ineligible.
derivation_service.go: requireWritePermission switches from pt.role='lead'
to pt.responsibility='lead' — project-management writes gate on the
project responsibility, not the firm tier. EffectiveProjectRole replaced
by UserProjectAuthorityLevel (thin wrapper over the SQL function in
migration 057). The legacy method was unused dead code despite t-139
design intent.
Tests extended: profession ladder, responsibility gate, NULL trap,
new validators. Build + vet clean.
|
||
|
|
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). |
||
|
|
fdde9eb754 |
feat(t-paliad-144 A2): frontend Custom Views UI
Phase A2 of the data-display-model rethink. Builds on A1's API contract
(merged as
|
||
|
|
cda4b4083d | Merge: t-paliad-144 A1 — backend substrate + Custom Views API (migration 056 paliad.user_views + ViewService 4-source union + FilterSpec/RenderSpec validators + SystemView registry + UserViewService + 9 HTTP handlers) | ||
|
|
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. |
||
|
|
bfc48b1420 |
fix(t-paliad-143): derived team members all show 'Attorney' + Herkunft collapses multi-unit users
Two related bugs on /projects/{id} Team tab → "Abgeleitet (Partner Unit)":
1. **All derived members labeled 'Attorney'.** Migration 055 added
partner_unit_members.unit_role with DEFAULT 'attorney' but never exposed
the column in the admin UI. So 100% of pum rows are 'attorney' and
Siemens AG's derive_unit_roles=['pa','senior_pa','attorney'] config
surfaces every member as 'attorney' even when they're really PAs.
2. **Multi-unit users collapsed to one source.** ListDerivedMembers used
ROW_NUMBER() OVER (PARTITION BY user_id) WHERE rn=1 — closest-attachment
wins, every other unit-membership dropped. Judith Molarinho Vaz +
Sabrina Franken belong to BOTH Lehment AND Plassmann; UI showed only one.
**Backend** (internal/services/derivation_service.go):
- DerivedMember.Memberships []DerivedMembership replaces scalar
UnitID/UnitName/UnitRole. DeriveGrantsAuthority becomes bool_or across
all source attachments (any granting → true).
- ListDerivedMembers SQL: jsonb_agg(DISTINCT jsonb_build_object(...)) +
bool_or(derive_grants_authority), GROUP BY user. One row per user, every
(unit, role) pair preserved. Memberships sorted by unit_name in Go (PG
doesn't allow ORDER BY inside DISTINCT-aggregated jsonb_agg).
- DerivedMembershipList implements sql.Scanner so the jsonb column maps
directly into the Go struct. Pinned by unit test.
**Frontend** (projects-detail.ts):
- DerivedMember interface mirrors the new shape. Herkunft renders every
(unit, role) source — single-unit users render as before
("über: **Lehment** [Sicht]"); multi-unit users render
"über: **Lehment** (Attorney), **Plassmann** (PA) [Sicht & 4-Augen]".
- Role column shows distinct unit_role values.
**Frontend** (admin-partner-units.ts):
- Member modal gains a per-row <select> with the 5 unit_role options. On
change, PATCH /api/partner-units/{id}/members/{user_id}/role (endpoint
already shipped in t-paliad-139 Phase 2). Disables during request,
rolls back the prior selection on failure.
- 2 new i18n keys (DE + EN): admin.partner_units.member.role,
admin.partner_units.feedback.role_updated.
- New CSS for .partner-unit-member-item flex layout + .pu-role-select.
**Out of scope** (per design): semantics of derive_unit_roles, new
unit_role values beyond the 5-row CHECK, the bigger profession-vs-project-
role redesign (#6).
**Verification**:
- Live SQL dry-run on Siemens AG (61e3fb9e-29fb-44aa-867e-a89469e2cacb)
returns Judith + Sabrina each with [{Lehment,attorney},{Plassmann,attorney}]
and derive_grants_authority=true (Plassmann grants authority).
- DerivedMembershipList.Scan unit-tested for nil / single / multi /
unsupported-type cases.
- Go build + tests pass; frontend build clean (1608 i18n keys).
After merge, m can verify on prod: /admin/partner-units → Plassmann →
set Judith to 'pa' → reload Siemens AG Team tab → Judith shows as 'PA'
with Herkunft "über: **Lehment** (Attorney), **Plassmann** (PA)".
|
||
|
|
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.
|
||
|
|
f8d8ea591d | Merge remote-tracking branch 'origin/main' into mai/noether/inventor-project | ||
|
|
d41fc49809 |
feat(t-paliad-139): Phase 1 — /projects/{id} aggregation bug fix
m's bug: /projects/{client_id} renders "Keine Fristen" / "Keine Termine" /
"Noch keine Ereignisse" even when descendant Cases carry deadlines, appts,
and audit events. Live verification on Siemens AG client
(61e3fb9e-29fb-44aa-867e-a89469e2cacb): 9 descendant projects, 19
deadlines, 37 project_events, 4 appointments — none on the Client row,
all invisible until now.
Root cause: 3 legacy per-project read paths used WHERE project_id = $1
(exact match), bypassing the projectDescendantPredicate primitive that
internal/services/visibility.go:68 already provides and that the t-124
union endpoints (DeadlineService.ListVisibleForUser etc.) already use.
Backend
-------
- DeadlineService.ListForProject(..., directOnly bool): subtree by
default via WHERE project_id IN (SELECT pp.id FROM paliad.projects pp
WHERE $1 = ANY(string_to_array(pp.path, '.')::uuid[])); collapses to
WHERE project_id = $1 when directOnly=true.
- AppointmentService.ListForProject: same shape.
- ProjectService.ListEvents(..., directOnly bool): same shape, plus
LEFT JOIN paliad.projects to surface project_title for the Verlauf
attribution chip on /projects/{id}. Inner subquery aliased pp to
avoid shadowing the outer join's p.
- models.ProjectEvent: new optional ProjectTitle string for the Verlauf
enrichment. Other readers leave it nil and the JSON serialiser omits
it (json:"project_title,omitempty").
- handlers/{deadlines,appointments,projects}.go: handler reads
?direct_only=true|false and passes through to the service. New
handlers.parseDirectOnly helper centralises the parse.
- project_filter_descendants_test.go: extended to also pin
DeadlineService.ListForProject + AppointmentService.ListForProject
+ ProjectService.ListEvents (live-DB test, skipped without
TEST_DATABASE_URL).
Frontend
--------
- projects-detail.ts: switched the deadline + appointment fetches from
/api/projects/{id}/deadlines + /appointments (legacy narrow) to
/api/events?type=deadline|appointment&project_id={id} (the union
endpoints, already aggregating + enriching with project_title). The
Verlauf still uses /api/projects/{id}/events but with the new
direct_only flag wiring.
- New subtreeMode state machine + URL param ?subtree=false. Default =
subtree (true). persistSubtreeMode replaceState keeps back-button
friendly.
- 3 new .subtree-toggle buttons in /projects/{id} History, Deadlines,
Appointments sections. Shared state across the three; clicking any
toggle reloads all three sections at once.
- attributionChip(rowProjectID, rowProjectTitle): inline chip "auf:
Case 14-vs-Müller" rendered when row.project_id !== currentProjectID.
Suppressed for direct rows.
- Deadline / Appointment / ProjectEvent interfaces gained an optional
project_title for the chip data path.
- 3 new i18n keys: aggregation.toggle.subtree (Inkl. Unterprojekte /
Incl. sub-projects), aggregation.toggle.direct_only (Nur direkt /
Direct only), aggregation.attribution.on (auf / on). DE+EN.
- global.css: .subtree-toggle, .subtree-toggle--active,
.aggregation-chip — small additive styling.
No schema. No migration. Phases 2 + 3 stack on top per design §7.
|
||
|
|
deef5aaff5 |
feat(t-paliad-138): CalDAV [PENDING] prefix + reminder digest pending banner
Commit 7 of 8. Outbound surfaces honour the pending-approval state instead of going silent on it. CalDAV (caldav_ical.go formatAppointment): when an appointment is approval_status='pending', the iCal SUMMARY line is prefixed with "[PENDING] ". External clients (Outlook, Apple Calendar, etc.) thus display the unverified state honestly. Approved entries sync clean. Email reminder digest (reminder_service.go): - digestRow gains ApprovalStatus, sourced from f.approval_status in the SELECT. - Each pending row's Title is rewritten to "[PENDING] <title>" before it lands in the template — visible in every email-rendered list. - Template data carries PendingCount (count of pending rows in this digest) + InboxURL so future template revisions can render a banner like "Hinweis: N Frist(en) wartet auf 4-Augen-Genehmigung — /inbox" without further code changes. Existing templates unchanged for backwards compat; the prefix on row titles already conveys the signal. - IsPending flag on each item map for future per-row template conditionals. Rationale: silence on a pending change is the worst outcome for a 4-eye system. The user's external calendar and reminder mail must reflect "this exists but isn't verified" so they can act before the deadline lapses. |
||
|
|
bc47d78d97 |
feat(t-paliad-138): pending pills on /events and /agenda
Commit 6 of 8. Renders the approval-pending warning pill on the two
busiest list surfaces:
- /events (deadline + appointment list): ⚠ pill next to the title +
soft-tinted row via .entity-row--pending-update modifier.
- /agenda (timeline): ⚠ pill in the headline + same row tint.
Changes:
- internal/services/event_service.go: EventListItem gains
ApprovalStatus *string; projectDeadline / projectAppointment
populate it from the embedded model.
- internal/services/deadline_service.go ListVisibleForUser: SQL adds
f.approval_status / pending_request_id / approved_by / approved_at
to the SELECT so DeadlineWithProject hydrates them.
- internal/services/appointment_service.go ListVisibleForUser: same
for appointments + completed_at.
- internal/services/agenda_service.go: AgendaItem gains
ApprovalStatus; the per-source SQL queries select it; the
loadDeadlines / loadAppointments projection sets it.
- frontend/src/client/events.ts renderRow: adds entity-row--pending-update
modifier and an inline approval-pill on the title cell when status='pending'.
- frontend/src/client/agenda.ts renderItem: same treatment on the
agenda-item headline.
Generic "pending update" label (approvals.pending_update.label) — not
lifecycle-specific. The inbox carries the lifecycle detail. Showing
just one pill keeps the visual signal clear; an approver scanning a
list of pending entities sees them at a glance via the row tint, then
clicks through to /inbox to see what's pending and act.
Detail pages (/deadlines/{id}, /appointments/{id}) and /dashboard
deadline rail — pill rendering for those surfaces deferred to a
follow-up to keep this commit focused. Rendered everywhere it
matters most for daily use.
|
||
|
|
07a1c17861 |
feat(t-paliad-138): /inbox page + sidebar bell badge
Commit 5 of 8. End-user surface for the approval workflow:
- /inbox page (frontend/src/inbox.tsx + client/inbox.ts) with two tabs:
"Zur Genehmigung" (requests I qualify to approve) and "Meine
Anfragen" (requests I submitted). Each row shows the project, entity
title, lifecycle event, requester name + age, the date-field diff
(for update/complete/delete) and the relevant action buttons:
approve + reject when on pending-mine, revoke when on mine.
Historic rows render a status pill instead of buttons.
- Sidebar bell entry "Genehmigungen" (with sidebar-inbox-badge) under
the Übersicht group. sidebar.ts polls /api/inbox/count every 60s and
shows the count (or 9+ ceiling) when > 0.
- Server registration: GET /inbox → dist/inbox.html, gated by
gateOnboarded. Already-registered API endpoints (commit 4) handle
the data path.
- Bilingual (DE primary / EN secondary) i18n strings under
approvals.* — labels, status names, lifecycle names, role names,
decision-kind names, action verbs, error messages. ~50 new keys.
- Pending-state CSS classes: .approval-pill, .approval-pill--historic,
.entity-row--pending-{create,update,complete,delete},
#sidebar-inbox-badge. Soft-tint rows + amber pill so an approver
can scan a list of pending entities at a glance. Used by commit 6
(pending pills across surfaces) — no other surface picks them up
yet, but the styles are wired and ready.
- Sidebar.tsx navItem signature gains an optional badgeID parameter
so any future sidebar entry can host a count-badge with one extra
argument (no per-entry custom rendering).
|
||
|
|
93c4453ce5 | Merge remote-tracking branch 'origin/main' into mai/cronus/inventor-dual-control | ||
|
|
457af2f6c4 |
fix(t-paliad-140): editable project on /deadlines/{id} + /appointments/{id}
Edit mode now exposes a project picker so a deadline or appointment can be moved to a different matter. Backend Update accepts project_id (and clear_project for appointments), validates visibility on the destination, and emits *_project_changed audit rows on both the OLD and NEW project so each side's Verlauf still shows the move. Personal-to-project linking and project-to-personal unlinking are gated by the existing personal-Appointment creator check; project-to-project moves re-use the existing requireMutationRole gate plus a fresh visibility check on the target. |
||
|
|
fb6a07f4b7 |
feat(t-paliad-138): approval API endpoints (policy CRUD + inbox + decisions)
Combined backend API for the upcoming policy-authoring page (commit 4)
and inbox + bell (commit 5). Registers:
Policy CRUD (admin-only via RequireAdminFunc gate):
- GET /api/projects/{id}/approval-policies
- PUT /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
- DELETE /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}
Inbox (any authenticated user; service-layer query gates by visibility
+ role-tier match):
- GET /api/inbox/pending-mine — requests I qualify to approve
- GET /api/inbox/mine — requests I submitted
- GET /api/inbox/count — bell badge count
- GET /api/approval-requests/{id} — one hydrated request
Decisions (caller authorization checked at service layer; the CHECK
constraint on approval_requests blocks self-approval as a second
defence):
- POST /api/approval-requests/{id}/approve
- POST /api/approval-requests/{id}/reject
- POST /api/approval-requests/{id}/revoke
Error mapping (writeApprovalError):
- ErrSelfApproval → 403 self_approval_blocked
- ErrNoQualifiedApprover → 409 no_qualified_approver
- ErrConcurrentPending → 409 concurrent_pending
- ErrNotApprover → 403 not_authorized
- ErrRequestNotPending → 409 request_not_pending
Frontend pages (the policy authoring tab on /projects/{id}/settings
and the /inbox page with bell) follow in subsequent commits — the
endpoints are usable via curl + admin tooling immediately.
|
||
|
|
10b3426086 |
feat(t-paliad-138): wire ApprovalService into deadline + appointment paths
Commit 3 of 8. The 4-eye gate now actually fires. With migration 054 applied and an approval_policies row configured for a project, the relevant Create/Update/Complete/Delete on a Deadline or Appointment flips approval_status='pending' and emits a *_approval_requested audit event. Without policies, behaviour is unchanged. Backend changes: - models.Deadline + models.Appointment gain approval_status, pending_request_id, approved_by, approved_at; appointments also gain completed_at (for the appointment:complete lifecycle event). - deadlineColumns + appointmentColumns include the new fields so every existing read path hydrates them via sqlx StructScan with no per-call-site changes. - DeadlineService gains SetApprovalService (nil-tolerant). Wired in main.go after the bundle is built. - AppointmentService gains the same hook + dependency. Lifecycle wiring: - DeadlineService.Create / Update / Complete / Delete each consult the approval gate. Update only triggers approval when a date-bearing field actually changes (Q4 allowlist: due_date, original_due_date, warning_date). Cosmetic edits (title, description, notes, rule_code, event_type_ids, status, completed_at via reopen) bypass. - AppointmentService.Create / Update / Delete same shape. Update only gates on start_at / end_at changes. Personal appointments (project_id IS NULL) never gate (no project policy to consult). - Delete is the one stage-then-write exception: the row stays alive with approval_status='pending' until the approver hard-deletes (approve) or restores it (reject). On no-policy projects, delete is immediate as before. - Concurrent-pending guard: any mutation on a row whose approval_status='pending' returns ErrConcurrentPending. The user must wait for the in-flight request to settle (or revoke if they're the requester). Pre_image capture: the date-bearing fields that are about to change are snapshotted into the approval_requests.pre_image jsonb at submit time. Reject/Revoke applies them back over the row to revert. |
||
|
|
4ebbf2c1af |
feat(t-paliad-138): ApprovalService core + tests
Commit 2 of 8 — the workflow engine for the 4-Augen-Prüfung. Wires the
service into the handlers.Services bundle so commit 3 can call into
SubmitCreate/Update/Complete/Delete from DeadlineService and
AppointmentService.
Public surface:
- Submit{Create,Update,Complete,Delete} — invoked by Deadline /
AppointmentService inside their existing tx. Looks up policy,
runs the deadlock check, inserts paliad.approval_requests, marks
the entity pending, emits the *_approval_requested project_events
audit row.
- Approve / Reject / Revoke — top-level operations (own tx). Approve
finalises the lifecycle (clears pending markers + sets approved_by
for non-delete; hard-deletes for delete). Reject / Revoke revert
the entity from pre_image (delete a pending-create, restore date
fields, NULL completed_at).
- ListPendingForApprover / ListSubmittedByUser / GetRequest /
PendingCountForUser — read paths the inbox + bell will hit in
commit 5.
- ListPolicies / UpsertPolicy / DeletePolicy — CRUD for the
authoring page in commit 4.
Self-approval is blocked at three layers:
1. canApprove() returns ErrSelfApproval when caller == requester.
2. The DB CHECK constraint approval_requests_no_self_approval.
3. The deadlock check excludes the requester from the pool.
Strict-ladder helper levelOf(role) mirrors the SQL function added in
migration 054. Path-walk authorization: ancestors with eligible roles
qualify for descendant requests (matches the visibility predicate).
Tests:
- Pure-Go: levelOf strict-ladder semantics, IsValidRequiredRole,
approvalEventType. All pass under `go test`.
- Live-DB (TEST_DATABASE_URL): no-policy noop; submit→approve cycle;
reject-create deletes; reject-update restores pre_image;
no-qualified-approver fail; revoke flow; policy CRUD roundtrip.
Skipped when TEST_DATABASE_URL is unset, mirroring the existing
audit_service_test pattern.
No call sites in DeadlineService / AppointmentService yet — that's
commit 3. Paliad continues to behave identically until that lands.
|
||
|
|
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. |
||
|
|
733917aae2 |
feat(t-paliad-122): GET /api/tools/courts + Fristenrechner court picker
GET /api/tools/courts[?courtType=UPC-LD] returns the deadline- computation slice of paliad.courts (id, code, names, country, regime, court_type) — distinct from the rich Gerichtsverzeichnis at /api/courts. Optional courtType filter narrows to a single tier. POST /api/tools/fristenrechner and POST /api/tools/fristenrechner/ calculate-rule both accept an optional courtId field. When set, the calculator resolves the court's (country, regime) and uses that calendar; when omitted, the proceeding's existing jurisdiction column seeds a sensible default — preserves today's behaviour for callers that don't yet send a court. Frontend: court-picker-row added to step 2 of the Fristenrechner wizard. Visible only for proceeding types with multiple compatible courts (today: every UPC-flavoured proceeding — UPC LDs span 12 countries, plus UPC CD seats and the CoA). DE-only proceedings (BPatG nullity, BGH appeals, DPMA, EPA, EP grant) keep the form unchanged. Picker re-runs the calc on selection so the user sees the same deadlines shift to a different calendar without a manual click. i18n key deadlines.court.label added for both DE and EN. Default courts wired sensibly: UPC_INF / UPC_REV / UPC_PI etc. → UPC LD München (HLC's home venue); UPC_APP / UPC_APP_ORDERS / UPC_COST_APPEAL → UPC CoA Luxembourg; UPC_REV → UPC CD Paris. |
||
|
|
d72990ad1b |
feat(t-paliad-122): country+regime aware HolidayService + CourtService
Holiday struct gains Country (ISO-3166) + Regime ('UPC' | 'EPO' | "")
fields. AppliesTo(country, regime) is the matching rule the new lookup
methods filter through: a row matches when its Country equals the
court's country OR its Regime equals the court's regime. UPC LD München
(DE+UPC) sees DE federal + UPC vacations; LG München (DE+"") sees only
DE federal; UPC LD Paris (FR+UPC) sees FR + UPC. germanFederalHolidays
fallback now country-tagged 'DE' so the per-country filter applies it
only to DE-jurisdictional callers.
Public service methods (IsHoliday, IsNonWorkingDay, AdjustForNonWorking
Days, AdjustForNonWorkingDaysWithReason, findVacationBlock) all take
(country, regime). Cache stays year-keyed — single DB hit per year, all
courts touching that year share it.
New CourtService loads paliad.courts once + answers Lookup(id),
CountryRegime(id, defaultCountry, defaultRegime), All(), ByCourtType(t).
FristenrechnerService.CalcOptions / CalcRuleParams gain CourtID;
EventDeadlineService.Calculate gains courtID. When courtID is empty,
DefaultsForJurisdiction maps the proceeding's existing jurisdiction
column to a sensible (country, regime) default — UPC proceedings get
(DE, UPC), everything else gets DE-only — preserving today's behaviour
for callers that don't yet send a court.
Tests: new TestAppliesTo_CountryRegimeFilter + TestAppliesTo_Rules
cover the cross-product of (DE court / UPC LD München / UPC LD Paris /
LG München) × (DE federal / UPC vacation / FR holiday). Existing tests
threaded through with ('DE', 'UPC') to preserve behaviour they were
written to lock.
|
||
|
|
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. |
||
|
|
b54e938bdf |
feat(t-paliad-136): Phase B — card-click → calc panel → add to project
The v3 result cards were dead-ends: clicking a Klageerwiderung pill
showed no deadline; users had to switch to Pathway A's wizard, retype
the date, and read the deadline out of the timeline. v4 makes the card
the entry to a single-rule calculator + add-to-project flow per m's
2026-05-05 11:58 feedback.
Backend (single-rule calc, no parent walk):
- New POST /api/tools/fristenrechner/calculate-rule endpoint accepts
either ruleId OR (proceedingCode + ruleLocalCode), trigger date, and
optional condition flags. Returns rule metadata + computed dueDate +
originalDate + adjustment-reason chip data.
- FristenrechnerService.CalculateRule() reuses the existing addDuration
+ HolidayService.AdjustForNonWorkingDaysWithReason pipeline so
t-paliad-119's adjustment-reason explainer and t-paliad-121's UPC-
Sommerferien skip both apply automatically. Court-determined rules
(party='court' or event_type ∈ hearing/decision/order) return
IsCourtSet=true and an empty due date.
- Flag-conditional rules surface FlagsRequired even when the caller
hasn't supplied the flag — the UI uses this to render checkboxes;
toggling recomputes live. With all flags satisfied + alt_duration_*
present, the calc swaps to alt values (existing semantics).
- Live-DB integration test covers plain calc, court-set, flag handling,
and error paths (skipped without TEST_DATABASE_URL).
Frontend (inline calc panel):
- Click any card body or rule pill → expand inline panel inside the
card (only one open at a time). Pill picker (radio chips) appears
when the card has 2+ rule pills; first preselected. Trigger date
defaults to today (m's Q3). Flag checkboxes auto-render from the
rule's condition_flag.
- Result row shows due date, "(N units from triggerDate)", and a
shift chip when wasAdjusted ("⚠ Verschoben vom … wegen UPC-
Sommerferien (27.7.–28.8.)").
- "Zu Akte hinzufügen" CTA → inline project picker → POST to existing
/api/projects/{id}/deadlines/bulk with a single-element array using
source='fristenrechner' (m's Q2: existing tag, no new audit category).
- Modifier-key clicks (Cmd/Ctrl/Shift/middle) preserve the legacy
drill-to-Pathway-A semantics via <a href> anchors. Trigger pills
(Wiedereinsetzung, etc.) keep the trigger-event drill — they don't
have a single rule to compute.
- Escape collapses the open card.
CSS: lime accent border on hover/expanded; dashed top border for the
calc panel; mobile-friendly grid for the pill picker.
UPC R.221 cost-appeal sequence (m's Q5) is wired in Phase C's seed
already; Phase B's pill picker renders both pills (leave-to-appeal +
notice-of-appeal) when the user hits one of those leaves.
|
||
|
|
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.
|
||
|
|
b7470d7d77 |
fix(t-paliad-136): Phase A — filter narrowing carries (concept, proc) tuples
The v3 B1 decision tree filter collapsed each leaf's (concept_id, proceeding_type_code) tuple list down to a flat concept_id slice in EventCategoryService.ConceptIDsForSlug, dropping the per-leaf proceeding constraint. The search service then loaded pills by concept_id only, so picking a UPC-specific leaf still surfaced DE/EPA/ DPMA pills for any shared concept (Klageerwiderung, Replik, Duplik, Berufungsschrift). m's repro: choosing CMS-Eingang → Gegenseite → UPC Verletzung leaked national submissions. Confirmed via DB: at least 25 leaves were over-broad pre-fix. Fix carries the tuple set end-to-end via a new subtreeFilter type with parallel uuid[] / text[] arrays. The matview SQL now uses unnest($cids, $procs) AS t(cid, pcode) to match each row against the allowed tuples — a junction row with NULL proc encodes "any proc for this concept" (used by cross-cutting concepts like Wiedereinsetzung). EventCategoryService gains AllOutcomes() for browse-all so the root view also respects junction tuples. allMappedConceptIDs is gone. Tests: added 5 v4 subtests under TestDeadlineSearch covering m's repro slug, multi-tuple narrowing, trigger-pill cross-cutting, forum AND-narrowing, plus an invariant regression gate that walks every leaf with non-NULL proc and asserts no pill leaks. Skipped when TEST_DATABASE_URL is unset; existing v3 assertions unchanged. No schema change. No migration. Ships independently of Phases B/C. |
||
|
|
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.
|
||
|
|
b32cfed37d |
feat(t-paliad-134): B1 surface — render concept cards beneath decision tree
Pathway B B1 mode previously rendered an empty result area on every
state — the runB1Search() output target was #fristen-search-results,
which lives inside the B2 panel. When B2 is hidden (B1 active), the
results were written into a hidden subtree and never seen.
Changes:
- TSX: add #fristen-b1-results inside #fristen-b1-panel, below the
cascade button row.
- frontend/fristenrechner.ts: extract renderSearchResultsInto() and
wirePillClicks(); runB1Search now writes to fristen-b1-results,
fetches /api/.../search?browse=all when no slug is picked yet (full
landscape on entry), and applies CSS-driven loading dim with a seq
guard against out-of-order responses. Hoisted loadAndRenderB1() so
showBMode("tree") can trigger the tree load on Pathway B entry
(radio.checked = true does not fire change events).
- backend: SearchOptions.BrowseAll, allMappedConceptIDs() returning
the union of every concept reachable from any leaf via
paliad.event_category_concepts, lifted limit ceiling for browse
modes (default 200, max 500). Handler exposes ?browse=all.
- CSS: shared loading-state styling for fristen-b1-results.
|
||
|
|
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 |
||
|
|
7141f4a954 |
feat(t-paliad-133): Phase C — B1 decision tree cascade + search extension
Wires the v3 Pathway B / B1 decision-tree cascade end-to-end. The
existing Phase D search backend gains two new query params, and the
frontend gets a data-driven button-cascade UI that walks
paliad.event_categories step-by-step.
Backend extension:
- internal/services/deadline_search_service.go
- SearchOptions gains EventCategorySlug + Forums fields.
- DeadlineSearchService gains an EventCategoryService dependency
via SetEventCategoryService(); wired in main.go after both
services exist (cross-link order).
- ForumToProceedingCodes map (10 buckets per m's spec lock §10 Q8)
translates v3 forum slugs to proceeding_type codes. Lives in Go
so rebucketing = code change, not migration.
- browseRanks() new query path: when q is empty AND
EventCategorySlug is set, synthesise rank rows from the slug's
reachable concept_ids — no trigram, just sort by
concept_sort_order. Drives B1 narrowing.
- rankConcepts() + loadPills() gain optional concept_id allow-list
+ forum_codes filters via UNIQUE NULLS NOT DISTINCT-shaped IS-NULL-OR
PARAM clauses. Trigger pills (kind='trigger') always pass forum
filter — they're cross-cutting by design.
- internal/handlers/fristenrechner_search.go
- Reads new ?event_category_slug= and ?forum= (comma-separated)
query params. Forwards to SearchOptions.
- parseCSV() helper.
Frontend B1 cascade:
- frontend/src/client/fristenrechner.ts
- loadEventCategoryTree(): one-shot fetch + in-memory cache of
/api/tools/fristenrechner/event-categories.
- renderB1Cascade(slug): renders breadcrumb + step question +
button row + skip-step + step-back. Buttons walk down, breadcrumb
walks back. Empty path = root question + 6 root buttons.
- runB1Search(slug): hits /api/tools/fristenrechner/search?event_category_slug=
and reuses Phase D's renderSearchResults() for the card list.
Empty-result path shows "Schritt zurück" link (m's spec lock §10 Q6
rephrase from "Pfad lockern").
- URL state ?b1=<dot-path> round-trips. popstate restores cascade.
- Pathway B default mode flips from filter → tree (B1 is now the
discovery surface; B2 is for power users).
Frontend i18n: +1 key (deadlines.pathway.b.tree.start_question).
Frontend CSS: .fristen-b1-breadcrumb, .fristen-b1-crumb,
.fristen-b1-question, .fristen-b1-buttons, .fristen-b1-button (with
--leaf modifier border-left accent), .fristen-b1-skip,
.fristen-b1-step-back rules.
Frontend build clean (1473 keys). go build + vet + tests clean.
|
||
|
|
2c770ef02f |
feat(t-paliad-133): Phase A — EventCategoryService + handler + route
Backend layer for the v3 decision tree:
- internal/services/event_category_service.go (NEW)
- Tree(): nested tree of all active event_categories for the
Pathway B / B1 cascade UI. Uses single SELECT + in-memory
parent-child stitching; corpus is small (≤100 nodes).
- ConceptsForSlug(): recursive CTE walks descendants of a slug and
joins event_category_concepts to return the candidate concept
outcomes (with optional proceeding_type_code narrowing).
- ConceptIDsForSlug(): convenience reduction for
`WHERE concept_id = ANY(...)` queries against the existing
deadline_search matview.
- ProceedingCodesForSlug(): per-leaf proceeding-code narrowing for
Phase D's forum filter intersection.
- internal/handlers/fristenrechner_event_categories.go (NEW)
- GET /api/tools/fristenrechner/event-categories returning the
nested tree as JSON. Frontend will ETag-cache via localStorage.
- Wired EventCategory into handlers.Services + dbServices + main.go.
The existing /api/tools/fristenrechner/search handler stays
unchanged in this commit; Phase D will add ?event_category_slug=
and ?forum= query params on top.
Build + vet clean.
|
||
|
|
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. |