0263a0e932e2e861907970c90a65971db48e630e
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
57237a55a3 |
feat(t-paliad-110): refactor Dashboard rails — drop Erledigt card, add Später + Termine rail
PR-4 of the Fristen+Termine unification, closing out t-paliad-110. Fristen rail (was 5 cards): - Erledigt card removed (status=completed stays reachable via the EventsPage filter dropdown — no card on the rail per the new model) - Später card added (pending deadlines past Mon-week-after, click filters to /deadlines?status=later) - 4+1 final shape: Überfällig (conditional alarm) · Heute · Diese Woche · Nächste Woche · Später Termine rail (new): 3 cards — Heute · Diese Woche · Später. No Überfällig (past appointments aren't urgent), no Nächste Woche (low-value distinction for appointments per the design rationale). Cards click through to /appointments?status=… so users land in the matching EventsPage view. Backend (DashboardService.loadSummary): - DeadlineSummary.CompletedThisWeek dropped, .Later added - AppointmentSummary added (Today / ThisWeek / Later) - One CTE-based query computes both rails alongside MatterSummary; bucket cutoffs share computeDeadlineBucketBounds with /api/events/summary + /api/deadlines/summary so all three surfaces stay in lockstep Frontend: - dashboard.tsx: Erledigt card removed, Später card + Termine section added - client/dashboard.ts: types updated, renderAppointmentSummary added - 4 new i18n keys (DE+EN): dashboard.summary.later + dashboard.appointment_summary.heading - CSS: .dashboard-card-later (muted blue) + 3 .dashboard-card-appt-* rules reusing the existing --bucket-* tokens go build/vet/test ./... clean. bun run build clean (1396 keys). |
||
|
|
37a925d3b2 |
feat(t-paliad-106): harmonize deadline summary — 5 disjoint buckets across Dashboard + Fristen
Both surfaces now show the same buckets with the same labels and the same cutoffs: Überfällig (conditional, alarming) · Heute · Diese Woche · Nächste Woche · Erledigt. Single-source bucket math via computeDeadlineBucketBounds — Heute = today, Diese Woche = tomorrow through the upcoming Sunday inclusive, Nächste Woche = next Monday through next Sunday inclusive, all disjoint. Items past next Sunday are visible only via "All open"/"Upcoming" filters; the Überfällig card stays hidden when count == 0 and switches to a saturated red pulse + bold white text when count > 0. Filter dropdown on /deadlines gains today / next_week entries; old "upcoming" filter still works as a back-compat alias for everything pending past this Sunday so legacy bookmarks don't 4xx. Tests: 8 deterministic table cases for the bucket pivots (every weekday + a 21-day disjointness walk). |
||
|
|
df321acb63 |
feat(t-paliad-097): clickable checklist references + Vorhandene Instanzen tab
Two related checklist UX gaps:
1. Checklist events in a project's Verlauf tab were unclickable — and
nothing in the project_events row carried the originating instance ID.
Add an `insertProjectEventWithMeta` helper, write
{"checklist_instance_id": <uuid>} as project_events.metadata for
checklist_created / _renamed / _linked / _unlinked / _reset (skipped
for _deleted — instance is gone). Surface metadata on
/api/projects/{id}/events and on dashboard recent_activity. The
Verlauf renderer wraps the title in <a href="/checklists/instances/{id}">
when metadata.checklist_instance_id is present, and the dashboard's
activity feed deep-links the project ref to the instance directly for
checklist_* events. Existing rows (metadata `{}`) stay non-clickable —
no migration backfill needed.
2. /checklists previously demanded a template pick before any existing
instance was reachable. Add a tab nav (Vorlagen / Vorhandene Instanzen)
using the existing entity-tab pattern. New endpoint
GET /api/checklist-instances and ChecklistInstanceService.ListAllVisible
return every visible instance across templates + projects, joined with
project ref/title and sorted by created_at DESC. Rows show template,
instance name (linked), project link (or "Persönlich"), progress bar,
and created date. URL state (?tab=instances) keeps the active tab
shareable. EN + DE i18n covered for tab labels and column headers.
Also adds event.title.checklist_* localizations for the Verlauf header
that translateEvent looks up.
|
||
|
|
460736ad1e |
refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed m/patholo → mAi/paliad → m/paliad, but go.mod still declared `mgit.msbls.de/m/patholo` and every internal import echoed the pre-rebrand name. Sweep: - go.mod: module path → mgit.msbls.de/m/paliad - All *.go files: imports rewritten via sed - README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad - Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx, global.css Verified: go build/vet/test ./... clean, bun run build clean, no remaining mgit.msbls.de/m/patholo or mAi/paliad references outside docs that intentionally describe the rename history. |
||
|
|
31db66e3b7 |
refactor(t-paliad-076): consolidate visibility predicate — 6 dashboard/agenda sites use helper
F-2 from t-paliad-074 audit. The inlined visibility predicate had drifted
back into 6 hot-path SQL sites despite the central helper extracted in
t-paliad-058. Consolidating now so future visibility changes (e.g.
Chinese-wall in design v2 §8) only need one edit.
**Sites converted (6):**
- dashboard_service.go:158, 214, 244, 274
- agenda_service.go:138, 204
All six replace `$N = 'global_admin' OR EXISTS (path-walk)` with the
existing `visibilityPredicatePositional("p", 1)` helper. The helper
resolves global_admin via EXISTS on paliad.users — the role string no
longer flows through positional args, removing one foot-gun (typo'd
literal mismatched against bound role) entirely. Equivalence verified
on the live youpc DB:
tester@hlc.de (global_admin, 1 team membership):
old predicate count = 11 new predicate count = 11
standard user (no team):
old predicate count = 0 new predicate count = 0
**No new helper variant added.** The audit suggested
`visibilityPredicateLateral`, but the existing positional helper drops
into the dashboard/agenda WHERE clauses unchanged — adding a redundant
variant would be technical debt. dashboard/agenda do not use LATERAL
JOIN; they use plain WHERE EXISTS in (sub-)SELECT context, which is
already what visibilityPredicatePositional emits.
**Other 4 sites flagged by audit — left intentionally:**
- reminder_service.go:312, 325 are role-restricted (`pt.role = 'lead'`)
membership checks, NOT visibility predicates. Adding a global_admin
shortcut to the lead branch would over-include rows: every global
admin would receive every project's lead-targeted reminder, even with
the `own.escalation_contact_id` override that exists precisely to
avoid that. global_admin already has its own dedicated branch in the
query (`$3 = TRUE AND own.escalation_contact_id IS NULL` at line 328).
- deadline_service.go:422 (`assertCanAdminProject`) is role-restricted
(`pt.role IN ('admin', 'lead')`) and already short-circuits global_admin
at the Go level before the SQL runs (line 413). Both halves correct;
no change needed.
- team_service.go:162 (`IsEffectiveMember`) was dead code with no callers
in the entire repo. "Is this user a structural team member?" and
"can this user see this project?" are different questions; adding a
global_admin shortcut would have conflated them. Deleted instead.
**Test:** new TestVisibilityPredicate_DashboardAgendaForGlobalAdmin in
visibility_test.go seeds a project + deadline + appointment + activity
event with project_teams empty, then asserts a global_admin sees all
four on /dashboard and /agenda while a standard user sees none. Skips
when TEST_DATABASE_URL is unset (matching the existing live-DB tests).
**Pre-existing finding (separate concern):** the live-DB test gate is
currently blocked locally by a stale `public.paliad_schema_migrations`
(version=2, dirty=t) left over from before the schema-pinned tracker
landed. Authoritative `paliad.paliad_schema_migrations` is at version
27, dirty=f. Out of scope for this task; should be filed as cleanup.
|
||
|
|
b34500ad31 |
feat(t-paliad-051): split paliad.users.role into job_title + global_role
Conflation: paliad.users.role was simultaneously job title (display only)
and global permission ('role=admin' checks across Go/SQL/JS). m wanted
to set his real job title ('Counsel Knowledge Lawyer') without losing
admin access — the t-paliad-050 admin-team UI even rejected role='admin'
on edit, so any UI-driven update silently demoted m.
Per m's three-axis principle ("firm roles are not project roles are not
tool roles"), this lands TWO orthogonal columns:
* paliad.users.job_title — free text, NULL allowed, display only.
NEVER gates anything in code or SQL.
* paliad.users.global_role — CHECK ('standard'|'global_admin'),
default 'standard'. The only thing that gates ops.
Migration 023:
* Drops NOT NULL + 'associate' default off the legacy role column
* Promotes role='admin' rows to global_role='global_admin'; clears
their role text; sets m's job_title='Counsel Knowledge Lawyer'
* Renames role -> job_title with CHECK (job_title IS NULL OR <> '')
* Replaces can_see_project body with global_role='global_admin'
* CASCADE-rebuilds every RLS policy under canonical English names —
with the historic u.role IN ('partner','admin') gates simplified
to u.global_role='global_admin' only (job_title NEVER gates)
Code surface:
* internal/models/models.go: User.Role -> User.JobTitle (*string) +
User.GlobalRole (string)
* internal/services/user_service.go: bootstrap (first row promoted to
global_admin via pg_advisory_xact_lock(7346298141), unchanged constant);
UpdateProfile drops role, accepts job_title only; AdminUpdateUser adds
global_role with last-admin demotion guard (ErrLastGlobalAdmin);
IsAdmin reads global_role
* Other services (dashboard/agenda/appointment/project/deadline/
department/party/note/checklist_instance): pass user.GlobalRole into
visibility predicates; partner-or-admin gates simplified to
global_admin only
* Handlers: drop now-impossible ErrAdminBootstrapOnly cases;
admin_users handles ErrLastGlobalAdmin -> 409
* department_service: SQL u.role -> u.job_title, DepartmentMember.Role
-> JobTitle (*string)
Frontend:
* /api/me + Me interfaces ship {job_title, global_role}
* Onboarding form: 'Berufsbezeichnung / Job title' (job_title)
* Settings + admin-team forms: same renames + i18n updates
* Admin-team: new 'Berechtigung / Permission' column with
'Standard'|'Global Admin' badge + dropdown editor; last-admin
demotion guard at the UI layer
* Sidebar admin-section reveal: me.global_role==='global_admin'
* deadlines/deadlines-detail/projects-detail/notes: partner-as-permission
gates dropped, only global_admin grants those operations
Tests:
* user_service_test: bootstrap promotes first user to global_admin,
subsequent default to standard; AdminUpdateUser refuses to demote
the last global_admin; IsAdmin reads global_role
Migration applied to ydb 2026-04-27. Live state verified:
* m: job_title='Counsel Knowledge Lawyer', global_role='global_admin'
* tester: job_title=NULL, global_role='global_admin'
* 29 stub colleagues: job_title='associate', global_role='standard'
|
||
|
|
5611e0154c |
fix(deadlines, appointments): /deadlines/{id} notfound + /deadlines list "Invalid Date" (t-paliad-039)
URGENT bug: /deadlines/{id} rendered "Frist nicht gefunden oder keine
Berechtigung" while the underlying /api/deadlines/{id} returned 200, and
/deadlines list showed "Invalid Date" in the date column.
Root causes — same class as t-paliad-038, this time on deadlines and
appointments client TS:
1. parseFristID/parseTerminID still checked URL prefix "fristen"/"termine".
After t-paliad-025 renamed pages to /deadlines and /appointments,
parts[0] no longer matched → null id → notfound branch fired before any
API fetch. Renamed to parseDeadlineID/parseAppointmentID with the
correct "deadlines"/"appointments" prefix.
2. fmtDate in deadlines.ts blindly appended "T00:00:00" to the API's
due_date string. After the v2 schema, the API returns full ISO
datetime ("2026-04-22T00:00:00Z"), and "...ZT00:00:00" is invalid →
"Invalid Date". Guarded both fmtDate and urgencyClass with
iso.length === 10 / iso.slice(0, 10).
3. Half-renamed variables (`let allDeadlines` declared, `allFristen`
used; `let deadline`, `frist` referenced). Worked at runtime only
because the undeclared identifier became a non-strict global. Cleaned
up to use the declared English names everywhere.
Lockstep DOM ID + variable rename in client TS + matching TSX:
- frist-* → deadline-* (deadlines-detail, deadlines, deadlines-new,
deadlines-calendar)
- termin-* / termine-* → appointment-* / appointments-* (appointments-detail,
appointments, appointments-new, appointments-calendar)
- fristen-body/empty/unavailable → deadlines-* (list page)
- termine-body/empty/unavailable → appointments-* (list page)
- frist-cal-grid / termin-cal-grid → deadline-cal-grid /
appointment-cal-grid (calendars)
- loadFristen/loadTermine/loadAkten/loadFrist/loadTermin/loadAkte →
loadDeadlines/loadAppointments/loadProjects/loadDeadline/loadAppointment/loadProject
- deadlines.ts: dropped unused projekt_office field from Deadline interface
- appointments.ts: dropped unused projekt_office field from Appointment
interface
Dashboard cleanup — Go service was still emitting `projekt_ref`:
- internal/services/dashboard_service.go: UpcomingDeadline /
UpcomingAppointment / ActivityEntry json+db tags `projekt_ref` →
`project_reference`; SQL aliases `AS projekt_ref` → `AS project_reference`.
- frontend/src/client/dashboard.ts: interfaces switched to
project_reference; activity link href /projects/{id}/fristen →
/deadlines, /termine → /appointments (the German per-project subpaths
were dead — t-paliad-038 already renamed projects-detail tabs).
i18n key strings (fristen.*, termine.*) intentionally kept in German per
the t-paliad-025 convention (frontend default language is German). CSS
class names (frist-row, frist-due-chip, frist-cal-cell, termin-dot,
termin-type-*, akten-table-wrap) untouched — separate stylistic cleanup,
no IDs are referenced in CSS so the rename is safe.
Verified: go build/vet/test clean, bun run build clean, dist HTML
contains only the new English IDs (remaining German strings are i18n
keys and product-name CSS classes).
|
||
|
|
3faec6c526 |
refactor(rename): German→English for backend (tables, types, services, handler files)
t-paliad-025 — Phase 1: backend rename.
Migrations 018+019 rewritten from scratch with English table/column
names throughout. Since v2 schema (018/019) has never been applied to
youpc prod DB, this is a clean replacement — not an ALTER RENAME chain.
Pre-existing German tables (parteien, fristen, termine, dokumente,
akten_events, notizen) are renamed inline in 018 via ALTER TABLE … RENAME
TO alongside the akte_id → project_id column rewrite.
Renames applied:
projekte → projects
projekt_teams → project_teams
projekt_events → project_events (via akten_events → project_events)
fristen → deadlines
termine → appointments
parteien → parties
notizen → notes
dezernate → departments
dezernat_mitglieder → department_members
dokumente → documents
can_see_projekt → can_see_project
notiz_is_visible → note_is_visible
akte_id / frist_id / termin_id / akten_event_id → project_id /
deadline_id / appointment_id / project_event_id
termin_type → appointment_type
Go types + services renamed:
Projekt / ProjektService / ProjektEvent / ProjektTeamMember
Frist / FristService / FristWithProjekt
Termin / TerminService / TerminWithProjekt / TerminType
Notiz / NotizService / ChecklistInstanceWithProjekt
Dezernat / DezernatService / DezernatMitglied
Partei / Parteien / ParteienService
Files renamed (git mv):
internal/services/{projekt,frist,termin,notiz,dezernat,parteien}_service.go
→ {project,deadline,appointment,note,department,party}_service.go
internal/handlers/{projekte,fristen,fristen_pages,termine,termine_pages,
notizen,dezernate,akten_pages,gerichte,glossar,checklisten}.go
→ {projects,deadlines,deadlines_pages,appointments,appointments_pages,
notes,departments,projects_pages,courts,glossary,checklists}.go
internal/checklisten/ → internal/checklists/
internal/db/migrations/018_projekte_v2.* → 018_projects_v2.*
internal/db/migrations/019_seed_dezernate_from_user_text.*
→ 019_seed_departments_from_user_text.*
User-facing i18n strings (DE/EN labels) stay untouched. Product names
Fristenrechner / Kostenrechner / Gebührentabellen stay German.
Build + vet + tests clean.
|
||
|
|
9aa8037193 |
refactor: services — Projekt, Team, Dezernat services (WIP Phase 2)
Models: Akte → Projekt (tree type + parent_id + path + client/matter numbers + netDocuments URL + type-specific client/patent/case columns). AkteEvent → ProjektEvent. FristWithAkte → FristWithProjekt. TerminWithAkte → TerminWithProjekt. Notiz.AkteID → ProjektID. ChecklistInstance.AkteID → ProjektID. Partei.AkteID → ProjektID. User adds AdditionalOffices pq.StringArray. Services: - NEW projekt_service.go replaces akte_service.go. Adds tree ops: List/GetByID/ ListChildren/ListAncestors/GetTree. Create auto-adds creator to projekt_teams role=lead in same tx. ResolveClientNumber walks path for inheritance. Visibility helpers (visibilityPredicate / Positional / Placeholder) centralise team-based access check: admin OR any ancestor/direct projekt_teams row. - NEW team_service.go — AddMember/RemoveMember/ListDirectMembers/ ListEffectiveMembers (unions direct + inherited via path, dedup by user; direct wins)/IsEffectiveMember. Inherited=true set at read time only. - NEW dezernat_service.go — admin-gated CRUD + member add/remove + user membership lookup for settings page. - frist_service.go → projekt_id everywhere, uses visibilityPredicate. ListFilter. AkteID → ProjektID. - termin_service.go → projekt_id everywhere. CalDAV log reads projekt_events. - notiz_service.go → projekt_id polymorphic branch; eventProjektID() looks at projekt_events; akten_event_id column kept (FK now resolves to projekt_events). - parteien_service.go → projekt_id. - checklist_instance_service.go → projekt_id with ClearProjekt flag. - dashboard_service.go → rewrites all four queries against projekte + projekt_events + projekt_teams. Matter/Upcoming/Activity surfaces use ProjektID/ProjektTitle/ProjektRef. - reminder_service.go → joins paliad.projekte, aliases a.reference AS akte_aktenzeichen for template compat. Handlers/tests still reference old API — Phase 2 completion requires handler rewrite (next commit). Build currently broken in internal/handlers. |
||
|
|
3e20806aee |
fix(security): verify JWT signatures + plug 4 other critical gaps (t-paliad-016)
C-1. Session JWT signature verification (authZ bypass fix) - Add SUPABASE_JWT_SECRET env var; fail-fast at startup if unset. - auth.Client.VerifyToken uses github.com/golang-jwt/jwt/v5 to verify HS256 signatures, reject alg=none, enforce exp/nbf/iat. - Middleware stores verified claims in request context; WithUserID reads only verified claims (no more raw-cookie sub decoding). - API requests get 401 on missing/invalid token (was 302 redirect). - Refresh flow only runs on expiry; other signature failures reject outright and clear cookies. C-2. Dashboard Termine cross-user privacy leak - dashboard_service.loadUpcomingAppointments now mirrors TerminService.canSee: personal Termine (akte_id IS NULL) are creator-only; admins do NOT see other users' personal Termine. C-3. Role gate on Parteien + Termine mutations - ParteienService.Delete now partner/admin only (matches FristService). - TerminService.Update / Delete on Akte-linked Termine now require partner/admin (or the original creator). Personal Termine stay creator-only. C-4. Email gate → ALLOWED_EMAIL_DOMAINS whitelist - isHoganLovellsEmail → isAllowedEmailDomain reading the env var (default: hoganlovells.com,hlc.com,hlc.de). Case-insensitive, whitespace-tolerant. - login.tsx placeholder: name@hoganlovells.com → name@hlc.com - Error strings + login.hint (de/en) rewritten for HLC branding. C-5. Docker compose env wiring - docker-compose.yml gains SUPABASE_JWT_SECRET, CALDAV_ENCRYPTION_KEY, and ALLOWED_EMAIL_DOMAINS passthrough; commented-out ANTHROPIC_API_KEY line for Phase H readiness. Tests - auth_test.go: valid/wrong-secret/expired/alg-none/missing-sub/garbage token cases for VerifyToken. - handlers/auth_test.go: default + env-override cases for the email whitelist. - go build ./..., go vet ./..., go test ./... all clean. |
||
|
|
b79ef258ef |
feat(dashboard): Phase G — logged-in landing page
New /dashboard route serves the authenticated home screen with a server-rendered payload (no skeleton→fetch waterfall, per design audit §2.3). / now redirects authenticated visitors to /dashboard and keeps the marketing landing for anonymous visitors. - DashboardService aggregates deadline + matter summaries, the next 7d of Fristen/Termine, and the last 10 akten_events, all scoped by the standard office-visibility predicate. - Dashboard handler splices the JSON payload into dist/dashboard.html as window.__PALIAD_DASHBOARD__ so the client paints on first frame; client re-fetches /api/dashboard every 60s to stay current. - Sidebar gains an "Übersicht" group with the Dashboard entry at the top; DE/EN i18n keys + traffic-light card styles added. - Empty-state copy, onboarding hint, and 503 handling keep the page intact when DATABASE_URL is unset. |