Commit Graph

58 Commits

Author SHA1 Message Date
m
9705290f3d Merge: Agenda — upcoming deadlines + appointments timeline
# Conflicts:
#	frontend/src/styles/global.css
2026-04-23 00:04:37 +02:00
m
f25113abe0 Merge: What's New changelog with sidebar badge
# Conflicts:
#	frontend/src/client/sidebar.ts
#	frontend/src/components/Sidebar.tsx
#	frontend/src/styles/global.css
2026-04-23 00:04:22 +02:00
m
0d6c58a337 feat(agenda): unified timeline of deadlines + appointments across projects
t-paliad-030. Adds `/agenda` — a single page that merges every visible
deadline and appointment into a day-grouped timeline, the third overview
surface alongside Dashboard and the per-resource lists.

- AgendaService: merges paliad.deadlines + paliad.appointments, gated by
  the same team-membership predicate used everywhere else; personal
  appointments stay creator-only. Items are sorted by date and tagged
  with urgency (overdue / today / tomorrow / this_week / later) so the
  client can apply the traffic-light colours without re-deriving buckets.
- GET /api/agenda?from&to&types and GET /agenda with the same server-side
  hydration pattern as /dashboard (JSON payload spliced into the shell so
  the timeline paints on first frame).
- Frontend: agenda.tsx + client/agenda.ts render a day-grouped timeline
  with type/range chips; filters round-trip through the URL.
- Sidebar entry under "Übersicht"; DE+EN i18n across all new keys.
2026-04-22 23:38:03 +02:00
m
9bb9f0c3df feat(search): global search across projects, deadlines, appointments, glossary, courts, checklists, links, users
Adds a sidebar-wide search bar (t-paliad-026) that hits a single GET
/api/search?q=... endpoint returning grouped results. Static content
(glossary, courts, link hub, checklist templates) is scanned in memory
against the curated Go slices; DB content (projects, deadlines,
appointments, checklist instances, users) is visibility-gated through
the same predicates the normal list endpoints use.

Frontend: new sidebar.ts-owned controller debounces 200ms, renders a
grouped dropdown, supports "/" to focus, Escape/arrows/Enter for
navigation, mobile-full-width overlay, and highlights matches.
2026-04-22 23:36:10 +02:00
m
94e2fc0024 feat(changelog): What's New page with sidebar badge
Adds a hardcoded changelog (internal/changelog) served via
GET /api/changelog and /api/changelog/unseen-count?since=<iso>, a
/changelog page that renders entries newest-first, and a sidebar
"Neuigkeiten" link with a lime badge showing the count of unseen
entries since the caller's last visit (localStorage stamp).

- internal/changelog: Entry struct, 11 pre-populated entries covering
  everything shipped so far (Dashboard, Projects/Deadlines/Appointments,
  CalDAV, Checklists v2, Glossary, Courts, Invitations, Settings,
  Paliad rename, and the changelog itself).
- Handler: public via auth-gated protected mux. Lexicographic string
  compare treats YYYY-MM-DD entries and ISO 8601 cutoffs symmetrically.
- Sidebar: new sidebar-changelog link before the Einladen button; the
  badge is populated by a fetch on every page load, suppressed on
  /changelog itself to avoid flash, and cleared on visit by stamping
  localStorage in changelog.ts's DOMContentLoaded handler.
- i18n: DE + EN keys for nav, page chrome, and tag labels.
- Unit tests for sort order, copy semantics, and same-day cutoff.

Task: t-paliad-027
2026-04-22 23:34:52 +02:00
m
caf319e7ee refactor(rename): frontend TSX + client TS files, fetch URLs, nav hrefs
t-paliad-025 Phase 3 — frontend rename pass:

File renames (git mv, preserving history):
  frontend/src/
    akten.tsx               → projects.tsx
    akten-neu.tsx           → projects-new.tsx
    akten-detail.tsx        → projects-detail.tsx
    fristen.tsx             → deadlines.tsx
    fristen-neu.tsx         → deadlines-new.tsx
    fristen-detail.tsx      → deadlines-detail.tsx
    fristen-kalender.tsx    → deadlines-calendar.tsx
    termine.tsx             → appointments.tsx
    termine-neu.tsx         → appointments-new.tsx
    termine-detail.tsx      → appointments-detail.tsx
    termine-kalender.tsx    → appointments-calendar.tsx
    einstellungen.tsx       → settings.tsx
    checklisten*.tsx        → checklists*.tsx
    gerichte.tsx            → courts.tsx
    glossar.tsx             → glossary.tsx

  frontend/src/client/ — same renames, plus notizen.ts → notes.ts.

Render exports renamed (renderAkten → renderProjects, renderFristen →
renderDeadlines, …). build.ts rewired to new names.

Client-side changes:
* fetch() API paths: /api/projekte → /api/projects, /api/fristen →
  /api/deadlines, /api/termine → /api/appointments, /api/notizen →
  /api/notes, /api/gerichte → /api/courts, /api/glossar → /api/glossary,
  /api/dezernate → /api/departments, /api/parteien → /api/parties,
  /api/checklisten → /api/checklists. Legacy /api/akten aliases removed.
* Navigation href/template strings: /akten → /projects, /fristen →
  /deadlines, /termine → /appointments, /einstellungen → /settings,
  /notizen → /notes, /checklisten → /checklists, /gerichte → /courts,
  /glossar → /glossary. Nested paths /neu → /new, /verlauf → /events,
  /kinder → /children, /kalender → /calendar, /dokumente → /documents.
* Interface names in client TS: Frist → Deadline, Termin → Appointment,
  Notiz → Note, Partei → Party, Akte → Project, ProjektMini → ProjectMini,
  Dezernat → Department, DezernatMitglied → DepartmentMember.
* JSON wire-format keys follow backend: projekt_id → project_id, akte_id
  → project_id, frist_id → deadline_id, termin_id → appointment_id,
  akten_event_id → project_event_id, dezernat_id → department_id,
  termin_type → appointment_type.

Go handlers (projects_pages.go, deadlines_pages.go, appointments_pages.go,
checklists.go, courts.go, glossary.go) serve the correctly-named HTML
files from dist/.

Kept German (user-facing i18n + product names):
* i18n keys/strings (src/client/i18n.ts) — DE labels and their keys
* Product names: fristenrechner, kostenrechner, gebuehrentabellen

Build verified: go build / vet / test clean; bun run build clean;
dist/ contains all 26 English-named HTML pages.
2026-04-20 17:44:45 +02:00
m
79889a2b83 Merge: remove Billing-Referenz UI + add Notizen (description) field 2026-04-20 17:17:18 +02:00
m
bde4b57099 feat: remove billing reference UI + add Notizen (description) field at every level
1. **Remove Billing-Referenz from the client create form.** Per m: the field stays
   in the DB (projekte.billing_reference column) but no longer in the UI. Dropped
   the input + label from akten-neu.tsx, the payload write from akten-neu.ts, and
   the projekte.field.billing_reference i18n keys (DE + EN).

2. **Add a Notizen (Notes) free-text field to project create + detail at every level.**
   Uses the existing projekte.description column (added in migration 018 — nullable
   text). Not to be confused with the polymorphic Notizen feature (threaded notes
   per projekt/frist/termin), which stays as-is.

   - akten-neu.tsx: textarea (rows=4) inserted above the Status select, rendered
     for every type (not type-conditional). akten-neu.ts: payload.description set
     on submit when non-empty.
   - akten-detail.tsx: new description block between header + tabs with
     akte-description-display (read) + akte-description-edit (textarea, edit mode).
     Edit/save flow on initTitleEdit extended to also PATCH description. Batch PATCH:
     only sends keys that actually changed.
     renderHeader() populates both, tags the wrapper data-empty=1 when nothing set
     (CSS can hide when empty if desired).
   - i18n: projekte.field.description / projekte.field.description.placeholder /
     projekte.detail.description.heading in DE (Notizen) + EN (Notes).

go build/vet/test + bun run build all clean.
2026-04-20 17:14:11 +02:00
m
ff1c5ceb0e fix(checklisten): consistent button sizing — Feedback uses outline variant of btn-cta-lime 2026-04-20 17:07:08 +02:00
m
59e1cb1445 Merge: i18n fallback fix + missing projekte.* translations 2026-04-20 17:06:38 +02:00
m
449075deaf fix(i18n): preserve default HTML text when key missing + add all projekte.* keys
Root cause: applyTranslations() in client/i18n.ts unconditionally overwrote
textContent/placeholder/title with t(key), and t() falls back to the raw key
name when no translation exists. Result: every projekte.* data-i18n attr in
the v2 pages rendered the literal key string ('projekte.heading',
'projekte.subtitle', ...) because I shipped the pages with new i18n keys
without adding the translations.

Two fixes, both in client/i18n.ts:

1. **Fallback behaviour**: applyTranslations() now uses a new internal
   tOrEmpty(key) that returns '' when the key is missing in DE and EN,
   and the call site only overwrites the DOM when the lookup yielded a
   real value. Missing keys no longer clobber the author-provided default
   text. This is belt-and-braces for any future page that ships a key
   before its translation does.

2. **Missing translations added**: ~90 projekte.* keys for DE and EN,
   covering the list page (projekte.heading/subtitle/new/search/filter.*/
   view.*/col.*/empty.*/unavailable), the create form (projekte.neu.*,
   projekte.field.*, projekte.cancel/submit/error.*), and the detail page
   (projekte.detail.title/back/loading/notfound/edit/save, tab.* for all
   eight tabs, verlauf.*, team.form.*/col.*, kinder.*, parteien.*
   form/role/col/empty, fristen.*, termine.*, checklisten.*, delete.*).

go build/vet/test + bun run build all clean.
2026-04-20 17:06:21 +02:00
m
adb0ce2c9d fix(modals): add padding to .modal-card — content no longer flush to edges 2026-04-20 17:05:53 +02:00
m
7e0c06342b feat: projekte-detail rewrite + tree view + per-Dezernat member manager (follow-ups)
**akten-detail.tsx rewrite (now projekte-detail-shaped):**
- Removed office-chip, firmwide-chip (v2 no longer uses them).
- Added type-chip, ClientMatter display (inherits via ancestors when absent),
  netDocuments external link.
- Breadcrumb nav above header, populated from /api/projekte/{id}/ancestors.
- New 'Untergeordnet' tab with children list from /kinder endpoint;
  'Untervorhaben anlegen' link pre-fills parent via ?parent=<id>.
- New 'Team' tab: lists direct + inherited members (inheritance badge
  shows ancestor title), remove button gated on self-or-partner/admin,
  add form with user typeahead and role picker.
- akten-detail.ts: Akte interface rewritten (reference/type/parent_id/
  path/client_number/matter_number/netdocuments_url/court/case_number).
  parseAkteID now accepts both /projekte/{id} and /akten/{id}. New loaders
  loadAncestors/loadChildren/loadTeam/loadUserList. TabId extended with
  'team' and 'kinder'.
- akten-neu.ts: applyParentFromQueryString pre-fills parent picker when
  navigated from a projekt's 'Untervorhaben anlegen' link, auto-switches
  type from 'client' to 'case'.

**Tree view in Projekte list:**
- Third view mode 'tree' alongside flat/roots. Sorts filtered rows by
  path (ancestors precede descendants); depth-indented title cell with
  ↳ branch glyph based on depthOf(path).

**Per-Dezernat member manager:**
- einstellungen Dezernat tab 'Verwalten' button now toggles an inline
  manage panel per Dezernat (expanded row below the admin table row).
- Panel shows current members with per-row remove (confirm dialog).
- Add-member form with user typeahead against /api/users, posts to
  /api/dezernate/{id}/members.
- Wires once per Dezernat (data-wired guard); reloads My Dezernat on
  any membership change.

i18n: DE + EN keys for dezernat.manage_heading/loading/no_members/
add_member*/add/remove/confirm_remove/error.user_required and for every
projekte.type.* / projekte.team.role.* / projekte.team.direct /
projekte.team.inherited.hint / projekte.view.tree / projekte.detail.team.*
/ projekte.detail.clientmatter.inherited.

go build/vet/test + bun run build all clean.
2026-04-20 15:35:01 +02:00
m
41cc295500 feat: Dezernate settings tab + best-effort seeding migration (Phase 4)
- einstellungen.tsx: fourth tab 'Dezernat'. My Dezernat card (name, office,
  lead, member list). Admin-only 'Dezernate verwalten' section with table
  (name/office/lead/members/delete) + 'Neues Dezernat anlegen' form behind
  a details summary. Admin controls hidden unless /api/me.role='admin'.
- client/einstellungen.ts: loadDezernatTab() fetches /api/dezernate, then
  per-dezernat /api/dezernate/{id}/members to resolve membership for the
  'My Dezernat' view. Admin table with delete-with-confirm. New-Dezernat
  form posts to /api/dezernate; inserts into in-memory list on success.
  TabName + TABS + loadedTabs dispatcher extended.
- i18n: dezernat.* keys (DE+EN) — heading/subtitle/admin section/table
  columns/form labels/error strings.
- Migration 019: best-effort seed of paliad.dezernate + dezernat_mitglieder
  from paliad.users.dezernat free-text. Each distinct non-empty name
  becomes one Dezernat (office = MIN(members.office)); every user whose
  free-text matches joins. free-text column preserved so a second pass
  can clean it up later. down-migration only deletes rows we inserted
  (matches name = btrim(user.dezernat)), leaves admin-created Dezernate
  alone.

go build/vet/test + bun run build all clean.
Branch mai/cronus/implement-data-model-v2 now covers all four phases.
2026-04-20 15:12:24 +02:00
m
640d5c1a23 feat: frontend v2 — Projekte list/create, dashboard + downstream field renames
- akten.tsx + client/akten.ts rewritten for v2: renders /projekte list with
  type filter (client/litigation/patent/case/project), status filter, flat
  vs roots view toggle, search across title/reference/client_number/
  matter_number. Columns now Title / Type / Reference / ClientMatter /
  Status / Updated (no more office column per v2 team-based visibility).
- akten-neu.tsx + client/akten-neu.ts rewritten: type selector drives
  conditional fields (client industry/country/billing; patent number +
  filing/grant dates; case court + case_number). Parent projekt picker
  (typeahead over /api/projekte, stores parent_id). ClientMatter
  client_number + matter_number (7-digit patterns) + netdocuments_url
  fields on every type.
- dashboard.ts field renames: akte_id → projekt_id, akte_title →
  projekt_title, akte_ref → projekt_ref. Activity/deadline/appointment
  links now point to /projekte/{id}.
- Mass field rename across fristen-*, termine-*, checklisten-*,
  fristenrechner.ts, notizen.ts, akten-detail.ts: akte_id → projekt_id,
  akte_aktenzeichen → projekt_reference, akte_title → projekt_title,
  akte_office → projekt_office. URL paths /akten/${...} → /projekte/${...}.

Pages still referencing deprecated shape (owning_office, collaborators,
firm_wide_visible) render blank for those columns — acceptable during
transition, full akten-detail rewrite (add breadcrumb + Team tab with
inheritance) still pending.

go build/vet/test + bun run build all clean.
2026-04-20 15:09:22 +02:00
m
4ac9dacaa0 feat: /projekte routes + sidebar label + legacy POST shim (Phase 3 partial)
Server-side:
- GET /projekte[...] routes alias the existing Akten list/detail/tab pages
  so users can reach the v2 URL without a 404 during the cutover. The TSX
  pages themselves still render the legacy HTML shell pointing at
  /api/akten legacy aliases.
- POST /api/projekte (and legacy POST /api/akten alias) now accepts BOTH
  old shape ({aktenzeichen, owning_office, court_ref}) and new shape
  ({reference, type, parent_id, client_number, matter_number,
  netdocuments_url, case_number}). aktenzeichen → reference,
  court_ref → case_number. owning_office is ignored (no longer part of
  visibility model).

Frontend:
- Sidebar nav link 'Akten' → 'Projekte' → /projekte.
- i18n: nav.projekte added (DE: 'Projekte', EN: 'Projects').

Still PENDING (Phase 3 remainder + Phase 4):
- Frontend TSX pages (akten.tsx, akten-detail.tsx, akten-neu.tsx etc.) still
  use legacy field names (aktenzeichen, owning_office, collaborators,
  firm_wide_visible). GET /api/akten returns NEW shape (reference, type,
  parent_id, path, client_number, matter_number, netdocuments_url). UI will
  display blank fields where the old column is missing. Full rewrite needed
  per task spec (tree view, type filter, breadcrumb, team tab with
  inheritance badges, client create form, projekt create with type +
  parent typeahead).
- Dezernate settings tab (Phase 4) not yet built — API endpoints exist at
  /api/dezernate[...] but no UI.
- Dashboard JSON shape changed (akte_id → projekt_id, akte_title →
  projekt_title, akte_ref → projekt_ref); frontend dashboard.tsx needs an
  update to read the new field names.

Build: go build/vet/test and bun run build all clean.
2026-04-20 14:55:06 +02:00
m
5fb55164b3 feat: settings page — profile, email preferences, CalDAV as tabs (t-paliad-022)
Unified /einstellungen page replaces the standalone CalDAV screen. Three
tabs today (Profil / Benachrichtigungen / CalDAV); adding more is additive
(one <a> in the tab nav, one <section> panel, one loader). Tab switching
is client-side from ?tab=<name> — default tab is Profil.

Profil tab lets users fix onboarding data without admin intervention:
display name, office, role, Dezernat, language. Email is read-only (the
source of truth is auth.users and an account-level change is out of
scope for the settings page).

Benachrichtigungen tab exposes deadline reminder preferences as a master
toggle plus three per-kind sub-toggles (overdue / tomorrow / weekly).
Preferences land in paliad.users.email_preferences (JSONB); missing keys
are treated as opt-in so existing users keep the behaviour they had
before the page shipped.

CalDAV tab is the old /einstellungen/caldav screen ported inline.
/einstellungen/caldav now 301-redirects to /einstellungen?tab=caldav so
bookmarks keep working.

Backend:
- PATCH /api/me (handlers/users.go) mutates the caller's paliad.users
  row. Attempts to include "email" in the body return 400 — the field is
  always server-authoritative.
- UserService.UpdateProfile builds a dynamic UPDATE from the pointer
  fields supplied; omitted keys are left untouched. Re-uses the
  admin-bootstrap guard for role changes.
- GetByID SELECT now includes lang + email_preferences so /api/me
  returns the data the settings page needs without a second round-trip.
- ReminderService consults email_preferences before sending — the helper
  reminderEnabled covers the master switch and per-kind overrides; corrupt
  JSON falls back to on so a bad row can't silence reminders.
- Migration 017 adds email_preferences jsonb NOT NULL DEFAULT '{}' and
  upgrades lang from nullable (from 016) to NOT NULL DEFAULT 'de' with a
  one-shot backfill. Down restores the nullable lang and drops
  email_preferences.

Model change: User.Lang moved from *string to string — it's NOT NULL in
the DB now, so the indirection was carrying no information. Inviter.Lang
and reminder row structs followed suit; the templates and callers used
""/"en" comparisons that translate 1:1.

Sidebar: the "Einstellungen" group now links to /einstellungen (instead
of just /einstellungen/caldav); the CalDAV sub-item is folded into the
tab nav on the page itself.

Tests: reminderEnabled has table-driven coverage (master switch,
per-kind, corrupt JSON, non-bool values). DB-backed user tests still
skip without TEST_DATABASE_URL as before.

Verified: go build ./..., go vet ./..., go test ./..., bun run build —
all clean.
2026-04-20 13:17:24 +02:00
m
11217f7bfa feat: email service — SMTP + deadline reminders + invitations (t-paliad-021)
- internal/services/mail_service.go: SMTP/TLS sender (implicit TLS on 465),
  html/template rendering, branded base layout + content templates, silent
  no-op when SMTP_* unset.
- internal/services/reminder_service.go: hourly scanner for Fristen that are
  overdue / due tomorrow / due within the week (Monday digest). Dedup via
  paliad.reminder_log (24h window).
- internal/services/invite_service.go: POST /api/invite flow with domain
  whitelist, in-memory 10/day/user rate limit, audit row in
  paliad.invitations.
- internal/handlers/invite.go: POST + GET /api/invite handlers.
- Sidebar "Kolleg:in einladen" button + modal on every page.
- migration 016: paliad.reminder_log, paliad.invitations, users.lang column.
- docker-compose: SMTP_* + PALIAD_BASE_URL env vars.
- docs/feature-roadmap.md: documented Supabase auth-SMTP routing as open
  question; current pilot keeps identity mails on Supabase default sender.

Rationale: get Paliad off Supabase's best-effort outbound for the
inbox-facing stuff (reminders, invitations) and move deadline nudges from
passive dashboard to active email. Custom Supabase auth SMTP is blocked on
the shared ydb.youpc.org instance — deferred until Paliad has its own
project or GoTrue webhook relay.
2026-04-20 12:34:38 +02:00
m
7c44bbec7e refactor: onboarding form — drop Praxisgruppe, free-text role, add Dezernat (t-paliad-020)
- Drop the Praxisgruppe field from the onboarding form. Every Paliad user
  is in patent practice, so the field carried no signal. The DB column is
  retained for future use (set to NULL on insert).
- Switch role from a 4-value enum (partner/associate/pa/admin) to free
  text with a <datalist> of suggestions (Partner, Associate, PA, Of
  Counsel, Referendar/in, Trainee, wiss. Mitarbeiter/in, Sekretariat).
  German firms have many roles beyond the original four.
- Add an optional Dezernat field — the team led by a specific partner.
  Free text, no FK (the partner may not be registered yet).

Backend:
- Migration 015: drop the role enum CHECK, replace with non-empty CHECK;
  ADD COLUMN dezernat text.
- UserService.Create: drop validRoles map, require non-empty role string,
  trim and persist Dezernat. Admin bootstrap gate unchanged.
- models.User gains Dezernat *string; userColumns SELECT updated so
  /api/me returns it.

Frontend:
- onboarding.tsx: replace role <select> with <input list=...>; add
  dezernat input; remove practice_group.
- onboarding.ts: send dezernat (if non-empty), require role.
- i18n: add onboarding.role.placeholder, onboarding.dezernat[.placeholder],
  onboarding.error.role; remove the role.* enum and practice_group keys.
2026-04-18 20:26:11 +02:00
m
b8f95f5d7a feat: user onboarding flow — first-login profile capture (t-paliad-019)
New users were stuck on the dashboard with a dead-end "Bitte schließen Sie das
Onboarding ab" message because nothing created the paliad.users row that all
matter-management features depend on. This adds the missing Phase D flow.

Backend
- UserService.Create: validates display_name / office / role, inserts the
  paliad.users row with (id, email) from the verified JWT claims (never from
  the request body — prevents onboarding as someone else).
- Admin bootstrap: only the very first paliad.users row may self-assign
  role='admin'; subsequent requests get ErrAdminBootstrapOnly (403). Guarded
  by pg_advisory_xact_lock so two concurrent first-logins can't race past
  the count=0 check under READ COMMITTED.
- POST /api/onboarding + GET /onboarding; the page is authenticated but NOT
  behind the onboarding gate (it's the one page users without a paliad.users
  row may reach).
- gateOnboarded middleware wraps the matter-management pages (Dashboard,
  Akten, Fristen, Termine, Einstellungen/CalDAV) and 302s to /onboarding
  when the caller has no paliad.users row. Knowledge-platform pages
  (Kostenrechner, Glossar, Links, Downloads, Gerichte, Gebührentabellen,
  Checklisten, Fristenrechner) stay ungated.
- auth.VerifiedClaims now carries the email claim; auth.ClaimsFromContext
  exposes it to handlers. GET /api/me includes the email in the 404 body so
  the onboarding form can pre-fill the display name from the local-part.

Frontend
- frontend/src/onboarding.tsx + src/client/onboarding.ts: centred card on the
  existing .login-card styling. Fields: display_name (required, pre-filled
  from email local-part), office (dropdown from /api/offices), role
  (dropdown, default associate), practice_group (optional).
- Dashboard client: toggleOnboardingHint now redirects to /onboarding
  instead of showing the dead-end hint — belt-and-braces behind the server
  gate in case the DB lookup fell through.
- DE + EN i18n keys for every label, placeholder, and error.
- Added onboarding to build.ts.

Tests: internal/services/user_service_test.go covers the valid path,
per-field validation, duplicate (ErrUserAlreadyOnboarded), and the
admin-bootstrap gate. Follows the existing TEST_DATABASE_URL skip pattern.
2026-04-18 19:13:57 +02:00
m
0cdc644b50 fix: audit medium items — Verlauf pagination, patholo→paliad rename, offices (t-paliad-018)
Three items from docs/improvement-audit.md §2:

I-5 Verlauf pagination
- AkteService.ListEvents now accepts a (before *uuid.UUID, limit int) cursor
- SQL uses a composite (created_at, id) cursor subquery — stable across
  rows written in the same microsecond
- Handler parses ?before=<uuid>&limit=<n>, service clamps to 200
- Frontend fetches first page (50) on init and exposes a "Mehr laden" /
  "Load more" button that keeps paging until the tail returns < page size
- i18n keys akten.detail.verlauf.loadMore / .loadingMore in DE + EN

I-8 patholo → paliad client-side rename with migrations
- i18n.ts: STORAGE_KEY is now paliad-lang; one-shot migration reads the
  old patholo-lang value, writes the new key, deletes the old
- sidebar.ts: same pattern for paliad-sidebar-pinned
- Cookie rename with dual-read grace period: SessionCookieName is
  paliad_session, LegacySessionCookieName keeps patholo_session as
  read-only fallback. Requests using the legacy cookie get upgraded to
  paliad_session in the response; legacy cookie is expired in the same
  response. ClearAuthCookies clears both names to prevent stale-cookie
  resurrection. Remove the legacy fallback after 2026-05-18 (30d cookie
  max age).
- handlers/links.go:extractEmailFromCookie reads either cookie name via
  auth.SessionCookieName / auth.LegacySessionCookieName

P-6 Single source of truth for offices
- New internal/offices package: Office struct + All + IsValid + Keys
- akte_service.go switched from inline isValidOffice to offices.IsValid
- GET /api/offices returns the list with DE + EN labels
- Akte create form (akten-neu.tsx) has an empty <select>; the client TS
  fetches /api/offices and populates options, re-rendering on lang change

Tests:
- internal/offices/offices_test.go covers IsValid + Keys + label coverage
- internal/auth: three new Middleware tests — legacy cookie still
  authenticates + upgrades the browser, new cookie wins when both are
  present (no clobber), missing cookie returns 401 on API paths

Build: go build ./... + go vet ./... + go test ./... + bun run build all clean.

Known out-of-scope: handlers/links.go still POSTs to public.patholo_link_*
via PostgREST; migration 011 created fresh paliad.link_* tables but the
handler refactor (move to direct DB, copy data, drop public tables) is a
separate phase documented in that migration's header.
2026-04-18 18:56:35 +02:00
m
67cd66e054 fix: audit quick wins — important + polish batch (t-paliad-017)
Items from docs/improvement-audit.md §2 + §3:

I-1  Hide Dokumente tab entirely from Akten detail (Phase H deferred);
     drop placeholder TSX panel, VALID_TABS entry, and orphaned
     akten.detail.soon.* i18n keys.
I-2  Add data-i18n keys for all 7 office labels on the landing page.
     EN mode now correctly renders "Milan" (was "Mailand").
I-3  Unify UPC URLs in Gerichtsverzeichnis to the canonical hyphenated
     form (unified-patent-court.org) matching links.go — 43 occurrences.
I-6  Add SEP/FRAND glossary category with 13 entries (FRAND, SEP,
     Standard-essentielles Patent, Patentpool, Anti-Suit, Anti-Anti-Suit,
     Injunction Gap, Orange-Book-Standard, Huawei/ZTE, RAND, ETSI IPR,
     Patent-Hold-up, Patent-Hold-out) + filter pill + suggest-modal option.
I-7  Refresh README: list migration 014 (checklist_instances), mark
     Phase I (Notizen) and Phase J (docs) shipped.
P-1  Remove HL Intern stub links (URL "#") and the now-empty "hl" category.
P-2  Dashboard heading: "Meine Mandate" → "Meine Akten" (matches CLAUDE.md
     naming convention). Onboarding hint updated likewise.
P-4  Drop "Hogan Lovells Patent Practice" from the footer — Paliad is the
     firm-agnostic brand.
P-5  Empty-state text on Fristen- and Termine-Kalender when the viewed
     month has no items.

Verified: bun run build clean, go build / vet / test ./... clean.
2026-04-18 09:14:43 +02:00
m
a7df6eb977 fix: resolve route conflict /api/checklisten/{slug}/instances vs /api/checklisten/instances/{id}
Move instance-specific endpoints to /api/checklist-instances/{id} to avoid
Go 1.22+ ServeMux ambiguity panic. Server was crash-looping.
2026-04-18 09:05:57 +02:00
m
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.comname@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.
2026-04-18 02:23:50 +02:00
m
117ccefe07 Merge: instanceable checklists — DB-backed, Akte-linked 2026-04-18 01:13:18 +02:00
m
4c0babb2f3 feat(checklisten): instanceable checklists — DB-backed state, Akte linkage
Checklisten move from one-per-slug localStorage state to a template/instance
model. A user creates multiple named instances of each template (UPC SoC,
EPA Einspruch, …), each with its own checkbox state in paliad.checklist_instances
and an optional akte_id for office-wide visibility.

- Migration 014: paliad.checklist_instances + RLS mirroring the Termine
  pattern (akte_id nullable → creator-only; akte_id set → can_see_akte gate).
- Static template data moves out of internal/handlers into internal/checklisten
  so both handlers and the new ChecklistInstanceService can reference it
  without an import cycle.
- ChecklistInstanceService: CRUD + state merge via `state || $n::jsonb`
  so concurrent checkbox toggles don't clobber each other. Reset clears
  state to {}. Akte-linked mutations append akten_events audit rows.
- Handlers: GET/POST /api/checklisten/{slug}/instances, GET/PATCH/DELETE
  /api/checklisten/instances/{id}, POST .../reset, GET /api/akten/{id}/checklisten.
- /checklisten/{slug} redesigned to show template metadata + instance
  table + "Neue Instanz" modal (with optional Akte dropdown). The
  interactive checkboxes move to /checklisten/instances/{id} where the
  state is DB-backed and Reset posts to the server. Fixes the original
  Reset button regression — it now operates on real server state rather
  than silently failing client-side.
- Akten detail grows a Checklisten tab listing linked instances with
  progress bars; only loads on tab activation.
- localStorage-based progress removed from the overview grid (state no
  longer lives there).
- DE + EN i18n keys added.

Verified: bun run build clean; go build ./...; go vet ./...; go test ./...
all green.
2026-04-17 13:54:32 +02:00
m
e96b9dfb77 fix(login): add autocomplete attributes to email/password fields 2026-04-17 13:47:33 +02:00
m
ee341742b6 fix(sidebar): disable horizontal scrolling on nav 2026-04-17 13:46:35 +02:00
m
42e5a8471c fix(sidebar): make nav scrollable when content overflows 2026-04-17 13:30:31 +02:00
m
5a9f8e5874 feat(notizen): Phase I — Notizen (polymorphic notes)
Polymorphic notes attached to Akten, Fristen, Termine, or AktenEvents.
Schema (paliad.notizen + paliad.notiz_is_visible) shipped with Phase A
migrations; this phase adds the service, handlers, and shared UI.

Backend
- NotizService (internal/services/notiz_service.go): ListForAkte /
  ListForFrist / ListForTermin / ListForAktenEvent + Create / Update /
  Delete. Visibility resolves through the parent row — AkteService.GetByID
  for Akte/Frist/AktenEvent parents, TerminService.GetByID for Termin
  parents (personal Termine are creator-only).
- Edit restricted to the original author; delete allows author +
  partner/admin. Create on an Akte-scoped parent appends an akten_events
  "notiz_created" audit row in the same transaction; personal Termin
  notes skip the audit.
- Author join (paliad.users) surfaces display_name + email on every
  listed note so the client can render "von <Name>" without per-row
  /api/users fetches.
- Routes wired in handlers.go: GET/POST /api/akten|fristen|termine/{id}/
  notizen, PATCH/DELETE /api/notizen/{id}.

Frontend
- Shared client module frontend/src/client/notizen.ts exposes
  initNotes(container, parentType, parentId). Renders an add-note form,
  list of note cards with relative timestamps (gerade eben / vor N
  Minuten / gestern / …), edit + delete affordances gated by author/
  role, optimistic add/edit/delete with rollback on error, Ctrl+Enter
  submit, and URL auto-linkification inside sanitised note bodies.
- Integrated into akten-detail (Notizen tab — placeholder replaced),
  fristen-detail (new "Notizen" section below the detail list), and
  termine-detail (new "Notizen" section above the edit form).
- DE + EN i18n keys added; obsolete akten.detail.soon.notizen placeholder
  keys removed.
- Notiz-card styles added to global.css (accent-coloured focus, hover
  actions, relative-time colour) matching the existing Verlauf card look.
2026-04-17 12:12:29 +02:00
m
b56ef660df feat(termine): Phase F — Termine (appointments) + CalDAV sync
Ship the appointments feature with bidirectional CalDAV synchronisation.
Closes KanzlAI audit §1.3 by encrypting CalDAV passwords at rest with
AES-256-GCM; plaintext credentials never touch the DB or API responses.

Backend
- `internal/services/termin_service.go`: CRUD with per-row visibility.
  Personal Termine (akte_id NULL) visible only to created_by; Akte-attached
  Termine follow AkteService.GetByID. Every Akte-attached mutation appends
  an akten_events row for the audit trail.
- `internal/services/caldav_service.go` (+ caldav_client.go, caldav_ical.go,
  caldav_crypto.go): per-user goroutine, 60s tick, push VEVENT + pull with
  UID/ETag reconciliation. Last-write-wins on conflict; conflicts on
  Akte-attached Termine append to akten_events.
- CALDAV_ENCRYPTION_KEY env var (32-byte AES-256, base64). Server refuses
  to start with malformed key; unset key leaves CalDAV disabled and all
  /api/caldav-config* endpoints return 501.
- Migration 013: paliad.user_caldav_config (password_encrypted bytea) +
  paliad.caldav_sync_log (last-5 per user). RLS: user owns their row only.
- HTTP handlers: GET/POST/PATCH/DELETE /api/termine, GET
  /api/akten/{id}/termine, /api/caldav-config CRUD + /test + /log.

Frontend
- Termine list / detail / new / kalender pages (Bun TSX + per-page client
  TS), calendar month grid with type-coloured dots and click-popup.
- Einstellungen/CalDAV settings page: URL/user/password (write-only),
  test-connection button, status card, sync log table, delete button that
  purges credentials.
- Akten detail "Termine" tab replaces the Phase D placeholder — inline
  add-termin form + list.
- Sidebar: Termine entry activated; new "Einstellungen" group with CalDAV.
- DE/EN i18n complete for every new surface.

Security posture
- AES-GCM with 12-byte random nonce prepended to ciphertext
- Password field has `json:"-"` on the model; API never returns it
- Frontend always sends password via write-only <input type=password>
- DeleteConfig purges the encrypted blob from the primary row
- TestConnection without stored creds requires explicit password

t-paliad-010
2026-04-17 11:59:49 +02:00
m
bebf79ba63 Merge Phase G: Dashboard landing
# Conflicts:
#	frontend/build.ts
#	frontend/src/styles/global.css
2026-04-16 17:30:26 +02:00
m
316dc9f9bf feat(fristen): Phase E — Persistent deadline management UI
Adds the persistent-deadline layer on top of the Phase A schema:

Backend (Go)
- internal/services/frist_service.go: CRUD + bulk import + summary
  counts, all gated through AkteService.GetByID for office-scoped
  visibility. Every mutation writes an akten_events row.
- internal/handlers/fristen.go: GET/POST/PATCH/DELETE for /api/fristen,
  /api/fristen/{id}, /api/fristen/{id}/complete, /api/fristen/summary,
  /api/akten/{id}/fristen, /api/akten/{id}/fristen/bulk.
- internal/handlers/fristen_pages.go: serves the four new HTML pages.
- Models: Frist + FristWithAkte (joined for the list page).
- Service wired into cmd/server/main.go.

Frontend (Bun TSX + per-page client TS)
- /fristen        — list with traffic-light summary cards (red/amber/
                    green), status + Akte filters, inline mark-complete.
- /fristen/neu    — create form (Akte select, due date, optional rule
                    + notes); /akten/{id}/fristen/neu pre-selects.
- /fristen/{id}   — detail with inline edit, complete, role-gated delete.
- /fristen/kalender — month grid with deadline dots + day popup.
- Akten detail "Fristen" tab now shows the real list (Phase D
  placeholder removed).
- Fristenrechner: "Als Frist(en) speichern" CTA opens a modal that
  picks an Akte + which calculated rows to import (POSTs to /bulk).
- Sidebar: activates the Fristen entry (was greyed-out in Phase D).
- DE/EN i18n for all new copy.
- Traffic-light + calendar styles in global.css.

Visibility, audit and role-gating reuse the Phase B/D primitives —
no new RLS or auth surface.
2026-04-16 17:28:44 +02:00
m
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.
2026-04-16 17:27:42 +02:00
m
4296da5583 feat(akten): Phase D — Akten (Mandate) CRUD UI
- TSX pages: list, create form, detail with Verlauf/Parteien tabs +
  Fristen/Termine/Dokumente/Notizen placeholders for future phases
- Client TS bundles for each page (search, filter, tab switching, inline
  title edit, party add/remove, delete-confirm modal, collaborator picker)
- Sidebar refactored into groups (Arbeit/Werkzeuge/Wissen/Ressourcen);
  Akten as first Arbeit entry; Fristen/Termine shown disabled with tooltip
- Backend: /api/me, /api/users, /api/akten/{id}/events + AkteService.ListEvents
- Server routes for /akten, /akten/neu, /akten/{id} and tab sub-routes
- i18n: full DE/EN strings for Akten UI + sidebar groups; title attr support
- Lime CTAs (#c6f41c), office badges, status chips, audit-trail feed
- Office-scoped visibility (firm_wide_visible partner-only, delete
  partner/admin-only) gated in UI; backend enforces regardless
- Graceful DATABASE_URL-unset message on list page; no 5xx
2026-04-16 15:27:52 +02:00
m
13da046709 refactor: rebrand patHoLo → Paliad (product name)
The Hogan Lovells merger makes the "HoLo" portmanteau obsolete. Paliad
(patent paladin) is firm-agnostic and survives future firm name changes.

- Page titles, logo/sidebar, footer, kostenrechner PDF branding
- All DE/EN i18n strings in frontend/src/client/i18n.ts
- README product line

Unchanged: repo/module/Go import paths, cookie names, Supabase table
names, localStorage keys, package.json name — all remain "patholo" as
internal identifiers. HL footer reference stays pending the post-merger
domain decision.
2026-04-16 11:12:45 +02:00
m
ec33287c45 Merge remote-tracking branch 'origin/mai/knuth/gerichtsverzeichnis'
# Conflicts:
#	frontend/build.ts
#	frontend/src/client/i18n.ts
#	frontend/src/index.tsx
#	frontend/src/styles/global.css
#	internal/handlers/handlers.go
2026-04-16 11:08:25 +02:00
m
4139ed1225 feat: Gerichtsverzeichnis — court directory for patent practice
41 courts: UPC Court of Appeal, Central Division sections (Paris/Munich/Milan),
13 Local Divisions, Nordic-Baltic Regional Division, 10 German courts
(LG, OLG, BGH, BPatG, DPMA), EPO (Munich HQ, Haar boards, Rijswijk), and
9 national courts (NL, UK, FR, IT). Addresses verified against official
sources; uncertain details left empty rather than guessed.

New page at /gerichte with search, dual filter pills (type + country),
expandable cards, print-friendly CSS, Supabase feedback (gerichte_feedback).

Migration at docs/migrations/002_gerichte_feedback.sql.
2026-04-16 10:51:02 +02:00
m
40ff17657f feat: Checklisten — interactive filing checklists with localStorage state
Six bilingual patent-workflow checklists (UPC Statement of Claim, Defence,
Confidentiality Application, Representative Registration; BPatG Nullity;
EPO Opposition) with grouped items, rule references, and tips. Index page
lists cards with regime filter and per-checklist progress; detail page
persists check state in localStorage (patholo:checklist:<slug>), shows a
live progress bar, supports reset and print, and submits feedback via
Supabase checklisten_feedback.
2026-04-16 10:48:42 +02:00
m
69d1402c0a feat: Gebührentabellen — interactive fee schedule reference with tabbed view 2026-04-14 22:47:04 +02:00
m
2a368a4b61 feat: Gebührentabellen — interactive fee schedule reference
Browsable, interactive fee tables for GKG, RVG, UPC, EPA, and PatKostG:

- New page at /tools/gebuehrentabellen with tabbed view
- API endpoint GET /api/tools/gebuehrentabellen returns all fee data
- Lookup endpoint GET /api/tools/gebuehrentabellen/lookup?streitwert=X
- GKG/RVG tables with version pills (2005, 2013, 2021, 2025/Aktuell)
- Streitwert input for quick lookup — highlights matching row
- UPC fee schedule (pre-2026 vs 2026) with fixed fees, value-based
  fees, recoverable costs ceiling, and SME reduction display
- EPA fees (opposition, appeal, grant, examination, search, filing)
- PatKostG tab with BPatG/BGH court fee factors, DPMA fees, and
  patent annual renewal fees (years 3-20)
- Common multipliers reference table (court fee factors, RA/PA VG/TG)
- Feedback modal with Supabase storage (gebuehrentabellen_feedback)
- Full DE/EN i18n, responsive layout, print-friendly
- Added to sidebar nav, landing page tools section, build pipeline
2026-04-14 22:44:55 +02:00
m
25e1f68d9e feat: Kostenrechner enhancements — PDF export, URL sharing, scenario comparison
PDF export: Branded print layout with patHoLo header, calculation metadata
(streitwert, date, VAT, selected instances), and disclaimer footer. Uses
@media print CSS for clean A4 output.

URL sharing: Encodes calculator state (streitwert, VAT rate, enabled instances
with all settings) in URL query params. Parses on load to restore state.
"Copy Link" button writes URL to clipboard and updates browser address bar.

Scenario comparison: "Compare" button snapshots current calculation as
Scenario A, shows side-by-side results panel. User modifies inputs for
Scenario B — results auto-update. Diff card shows cost delta with per-category
breakdown (court fees, attorney fees, etc.) and percentage change. Green/red
color coding for savings vs. cost increase.

All labels i18n'd in DE and EN.
2026-04-14 22:37:23 +02:00
m
c8ad74ff17 feat: Patentglossar — searchable DE/EN glossary with term suggestions 2026-04-14 20:11:00 +02:00
m
f66a2ed906 feat: Link Hub — curated external links page with collaborative features
New /links page with 22 curated links across 5 categories:
- Gerichte & Ämter (UPC CMS, EPO, DPMA, BPatG, EUIPO)
- Recherche (Espacenet, DPMAregister, DEPATISnet, Google Patents, WIPO)
- UPC (Rules of Procedure, Fees, Practice Directions)
- Gesetze (PatG, EPÜ, UPCA, GKG, RVG, ZPO via dejure.org)
- HL Intern (placeholder links)

Features:
- Client-side category filter tabs
- "Link vorschlagen" button with modal form (POST /api/links/suggest)
- Per-card feedback icon with modal (POST /api/links/feedback)
- Pending suggestion count badge
- Full DE/EN i18n support
- Static link data served via GET /api/links (Go map)
- Supabase PostgREST integration for suggestions/feedback storage
- Sidebar nav entry with chain-link icon

Supabase migration in docs/migrations/001_link_suggestions.sql
(needs to be applied on ydb.youpc.org before collaborative features work).
2026-04-14 20:07:43 +02:00
m
0960235bb8 feat: searchable DE/EN patent glossary with term suggestions
- New /glossar page with 73 bilingual patent law terms across 5 categories
  (Litigation, Prosecution, UPC, EPA, General)
- Client-side instant search filtering both DE and EN columns
- Category filter pills for quick narrowing
- Suggest-a-term button opens modal form for new term submissions
- Per-term feedback icon for correction suggestions
- Suggestions stored in Supabase (glossar_suggestions table with RLS)
- Go API: GET /api/glossar (terms JSON), POST /api/glossar/suggest
- Full DE/EN i18n support, responsive layout, print-friendly
- Added to sidebar nav, landing page tools section, and build pipeline
2026-04-14 20:06:51 +02:00
m
fd25998f70 feat: dedicated downloads page 2026-04-14 19:33:29 +02:00
m
cb0e61f0bf feat: dedicated downloads page with card grid layout
New /downloads route behind auth with Sidebar, i18n DE/EN,
and download card for HL Patents Style.dotm. Structured so
adding more files is a one-liner in the files array.
2026-04-14 19:32:07 +02:00
m
8419dec467 fix: add Hamburg, Paris, Milan to Standorte 2026-04-14 19:30:44 +02:00
m
d5aae0aabb feat: sidebar navigation — collapsible icon rail with hover expand and mobile overlay 2026-04-14 19:27:55 +02:00
m
0f1976b146 feat: replace header with sidebar navigation on authenticated pages
Sidebar layout with collapsible icon rail (64px) that expands to 240px
on hover (150ms delay) or pin (persisted in localStorage). Mobile
(<1024px) uses hamburger FAB with overlay. Active page highlighted.
Login page retains the original Header component.

New: Sidebar.tsx, sidebar.ts
Changed: index/kostenrechner/fristenrechner pages, global.css, i18n
2026-04-14 19:15:26 +02:00