Commit Graph

551 Commits

Author SHA1 Message Date
m
1d7c7d7246 Merge: t-paliad-151 Phase B code (env-var-gated, compose flip held for A.5) — Paliadin remote-routing via Tailscale SSH to mRiver. Includes Phase A.0 design doc + scripts/paliadin-shim from earlier shift. Production behavior unchanged: without PALIADIN_REMOTE_HOST in env, paliad never invokes ssh and uses local-tmux PoC path byte-identically. Refactor: Paliadin interface + LocalPaliadinService + RemotePaliadinService + DisabledPaliadinService stub. main.go env-var switch (remote/local/disabled). Dockerfile +openssh-client. 14 unit tests via callShimHook. Frontend friendlyErrorMessage for mriver_unreachable/shim_auth_failed/shim_error/bootstrap_failed/timeout (DE+EN). NOT included: docker-compose network_mode: host flip — held on branch as da971a7 pending Phase A.5 traefik test by m. NOT cronus. 2026-05-08 02:23:38 +02:00
m
e4110cf2db feat(t-paliad-151) frontend: friendly errors for remote-Paliadin codes
Extends the SSE error switch in frontend/src/client/paliadin.ts'
friendlyErrorMessage to map four new error codes from RemotePaliadin
Service into localised messages:

- mriver_unreachable: mRiver is offline / paliadin-shim unreachable
  (DE: "mRiver ist offline — Paliadin nicht erreichbar. Mach mRiver an,
  oder nutze Paliadin lokal mit ./paliad."
   EN: "mRiver is offline — Paliadin can't reach it. Wake mRiver, or
  run Paliadin locally with ./paliad.")
- shim_auth_failed: SSH key / authorized_keys mismatch (Permission
  denied)
- shim_error / bootstrap_failed: generic remote-shim failure
- timeout: Claude didn't write the response file in 60 s

Adds the matching i18n keys (DE + EN) plus the type-union entries in
i18n-keys.ts so the t() typecheck stays sound. The old codes
(tmux_unavailable, connection_lost, upstream) are unchanged — local-PoC
deployments keep their existing UX.

Frontend `bun run build` clean: 1886 keys (unchanged sync).

Refs m/paliad#12
2026-05-08 02:19:48 +02:00
m
68c56ea920 test(t-paliad-151): paliadin_remote_test.go — RemotePaliadinService unit tests
14 tests covering:
- NewRemotePaliadinService default values (SSHPort=22022, SSHUser="m")
- NewRemotePaliadinService honours overrides
- classifySSHError mapping (nil / explicit + wrapped ErrMRiverUnreachable
  / context.DeadlineExceeded / shim exit-124 timeout / Connection
  refused/timed out / Permission denied / unknown fallback)
- healthGate caches OK results for 10 s
- healthGate does NOT cache failures (every call re-probes)
- healthGate rejects unexpected shim replies (returns wrap of
  ErrMRiverUnreachable)
- healthGate cache expires after 10 s wall clock
- ensureBootstrapped runs exactly once on success (idempotent)
- ensureBootstrapped retries after failure, then caches the success
- DisabledPaliadinService returns ErrPaliadinDisabled from RunTurn +
  ResetSession
- compile-time Paliadin interface conformance for all three impls
- callShim forwards args verbatim through the test hook
- callShim error-wrapping path preserves stderr (so classifySSHError
  can pattern-match Permission denied / Connection refused etc.)

All tests bypass exec via the callShimHook field — no real ssh, no
real DB. RunTurn audit-row tests are out of scope (paliad has no
sqlx mock; existing paliadin_test.go also stays on pure functions).

Refs m/paliad#12
2026-05-08 02:18:08 +02:00
m
0c8a2f1a95 feat(t-paliad-151) RemotePaliadinService + main.go env-var routing
Phase B step 2: lands the Paliadin backend that talks to mRiver via
ssh + paliadin-shim. Local backend untouched — selection happens in
cmd/server/main.go based on PALIADIN_REMOTE_HOST.

Files:
- internal/services/paliadin_remote.go (new) — RemotePaliadinService
  + RemotePaliadinConfig, with five SSH knobs (Host/Port/User/KeyPath/
  KnownHostsPath). RunTurn does insertTurnRow → healthGate → bootstrap
  → callShim run-turn → splitTrailer → completeTurn, mirroring the
  local path's audit-row contract. ResetSession sends shim 'reset'.
  callShim runs `ssh -F /dev/null -i <key> -p <port> -o … host -- verb
  args`; ControlMaster intentionally not enabled (design §6.8).
- internal/services/paliadin_remote.go also adds DisabledPaliadinService
  (returns ErrPaliadinDisabled from RunTurn/ResetSession; DB methods
  inherited from paliadinDB still work) so cmd/server/main.go can wire
  a non-nil Paliadin even when neither local tmux nor remote SSH is
  available.
- ErrMRiverUnreachable sentinel for the friendly error code.
- classifySSHError translates ssh exit 124 / Permission denied /
  network errors into the audit-row error_code field.
- Compile-time conformance: var _ Paliadin = (*Local|*Remote|*Disabled)
  PaliadinService(nil).

cmd/server/main.go switch:
  PALIADIN_REMOTE_HOST set → NewRemotePaliadinService
  else: tmux on PATH → NewLocalPaliadinService
  else: NewDisabledPaliadinService

buildPaliadinRemoteConfig materialises PALIADIN_SSH_PRIVATE_KEY +
PALIADIN_KNOWN_HOSTS (multi-line Dokploy secrets) into chmod-600/644
tmpfiles at boot. Defaults: SSHUser=m, SSHPort=22022 (bypasses
Tailscale SSH on :22, see design §4.5). Fails fast on a configured
remote-host without the matching key/known_hosts secrets.

Local-tmux mode now requires `tmux` actually be on PATH at boot
(exec.LookPath gate); previously the constructor unconditionally
returned a service whose RunTurn would fail at runtime with
ErrTmuxUnavailable. The handler-level "friendly error" UX is
unchanged: DisabledPaliadinService surfaces ErrPaliadinDisabled which
the frontend renders the same way.

Build green; existing paliadin_test.go still passes (it tests
package-level helpers, untouched). Remote-specific tests land in B4.

Refs m/paliad#12
2026-05-08 02:16:50 +02:00
m
56a3dc961e refactor(t-paliad-151): extract Paliadin interface; rename PaliadinService → LocalPaliadinService
Phase B step 1 of the Tailscale-SSH route to mRiver. Splits the existing
local-tmux PoC into a Paliadin interface with two implementations; the
remote-SSH backend lands in a follow-up commit (paliadin_remote.go).

Surface:
- Paliadin interface — RunTurn, ResetSession, ListRecentTurns, Stats,
  IsOwner. The handler at internal/handlers/paliadin.go now talks to
  this instead of the concrete struct.
- paliadinDB — embedded base type carrying the audit-table I/O
  (insertTurnRow, completeTurn, markTurnError, markTurnAbandonedOrError)
  plus the read-side queries (IsOwner, ListRecentTurns, Stats). Both
  Local and Remote impls inherit these by embedding paliadinDB so the
  remote path doesn't have to duplicate any DB code.
- LocalPaliadinService — the renamed PoC backend. Identical behaviour
  to the previous PaliadinService; only the type name and method
  receivers change. Method receivers split: tmux-specific operations
  (RunTurn, ResetSession, ensurePane, sendToPane, pollForResponse, etc.)
  stay on *LocalPaliadinService; DB-only operations promote to
  *paliadinDB.

Wiring:
- internal/handlers/handlers.go — Paliadin field becomes the interface
  type; Register() unchanged.
- cmd/server/main.go — calls NewLocalPaliadinService instead of
  NewPaliadinService. The remote-vs-local switch on PALIADIN_REMOTE_HOST
  lands in B5.

Tests in paliadin_test.go all green — they test package-level functions
(splitTrailer, countChips, approxTokenCount, sanitiseForTmux,
PaliadinOwnerEmail) and don't touch the renamed struct. No behaviour
change on the local-tmux path.

Refs m/paliad#12
2026-05-08 02:14:12 +02:00
m
f62bf9f8fb feat(t-paliad-151) Dockerfile: openssh-client for remote Paliadin
paliad's RemotePaliadinService shells out to `ssh m@mriver paliadin-shim`
to deliver Paliadin turns from prod (paliad.de Dokploy container) to
mRiver where the long-lived tmux+claude pane lives. The alpine final
stage didn't ship an SSH client; add openssh-client (~1.1MB compressed).

The Go service wires this up in a follow-up commit (Paliadin interface
split). When PALIADIN_REMOTE_HOST is unset, the binary still picks up
the local-tmux PoC path and never invokes ssh, so this change is safe
on its own.

Refs m/paliad#12
2026-05-08 02:10:40 +02:00
m
dd139a3536 Merge remote-tracking branch 'origin/main' into mai/noether/inventor-paliadin 2026-05-08 02:08:12 +02:00
m
f952fb85c3 design(t-paliad-151) amend: port 22022 bypass + Phase A.0 results
Phase A.0 revealed Tailscale SSH on mRiver intercepts :22 from tailnet
peers and bypasses OpenSSH's authorized_keys entirely (banner
"SSH-2.0-Tailscale", auth method "none", command= restriction never
fires). The fix is port 22022 via a systemd ssh.socket drop-in:
Tailscale SSH only intercepts :22, so :22022 hits real OpenSSH where
the design's command=/from= shim restriction works as specified.

Updated:
- §3 locked decisions: row 5 added (port 22022, m's call 23:35)
- §4.5 new subsection: Tailscale SSH bypass via socket drop-in
  + records the "Address already in use" first-attempt failure as a
  "don't retry without cleaning sshd_config Port directives first"
  lesson
- §5.2/5.3: ssh-keyscan now uses -p 22022; known_hosts is host:port
  keyed for non-22 ports
- §6.1/6.2/6.3: SSHPort field on RemotePaliadinService config, -p
  flag in callShim, PALIADIN_REMOTE_PORT env (default 22022)
- §7 phasing: A.0 completion checked off step-by-step with concrete
  fingerprints; A.5/A.6/A.7 split out as m-driven
- §8 security: Tailscale-SSH-on-:22 risk explicitly tabled with
  port-22022 mitigation
- §10 deliverables: mRiver host-setup artifacts noted
- §12 new Phase A.0 completion summary with the three secrets m
  needs to register in Dokploy

Phase A.0 verified end-to-end:
- ssh -p 22022 paliad-prod-key m@mriver health → ok
- run-turn UUID base64msg → 3.4 s including a real Claude response
- from="100.99.98.201" correctly rejects connections from mRiver
  itself

mRiver host state in place (not in repo): authorized_keys with
restrictions, /home/m/.local/bin/paliadin-shim, ssh.socket drop-in.
Three secrets staged at ~/.paliad-staging/ on mRiver for m to copy
into Dokploy: paliad-prod-key (PALIADIN_SSH_PRIVATE_KEY),
known_hosts (PALIADIN_KNOWN_HOSTS), and the three plain env vars.

Refs m/paliad#12
2026-05-07 23:37:26 +02:00
m
b78941e293 Merge: t-paliad-152 — /api/events honours direct_only (Fristen/Termine subtree toggle works again — handleListEvents + handleEventsSummary parse direct_only via parseDirectOnly; threaded as DirectOnly bool through EventListFilter / EventSummaryFilter / ListFilter / AppointmentListFilter; project predicate swaps from projectDescendantPredicate to direct project_id eq when set; 3 new DirectOnly subtests in project_filter_descendants_test.go) 2026-05-07 23:21:01 +02:00
m
55c93c9de3 Merge: t-paliad-153 — Frist due_date 02:00 leak (consolidate views/format.ts with UTC-anchored date-only detection + kind-aware formatRowTime/formatRelative; shape-cards skips time slot under day-grouped headings; shape-list reduces deadline relatives to day precision; tests pass under TZ=Berlin/LA/UTC) 2026-05-07 23:08:18 +02:00
m
f90bfeda9b fix(t-paliad-153): deadline due_date renders 02:00 in CEST (UTC-midnight leak)
Substrate marshals deadline.due_date as time.Date(...,0,0,0,0,UTC), so the
JSON arrives as "YYYY-MM-DDT00:00:00Z" — UTC midnight, no real time. Feeding
that into new Date() + toLocaleTimeString() produced "02:00" in CEST,
"01:00" in CET, "20:00 the day before" in EST, etc.

Pattern A: don't render time for date-only fields.

- Centralised the date/time formatters used by the views shapes into
  frontend/src/client/views/format.ts. parseDateOnly recognises both
  "YYYY-MM-DD" and the substrate's "YYYY-MM-DDT00:00:00Z" form; formatDate
  formats those in UTC so the day matches the source day in every timezone.
- shape-cards.ts: per-row time slot is empty for deadlines when the day is
  already in the heading (groupBy=day). Falls back to formatDate when
  groupBy=week|none. Bucketing now anchors date-only inputs to UTC so a
  deadline can't slip into the previous day in negative-offset zones.
- shape-list.ts: formatRelative is kind-aware — deadlines reduce to
  day-precision ("morgen" / "in 3 Tagen") instead of leaking hour math
  ("in 2h") off the UTC midnight.
- Appointments and other timestamped sources are untouched.
- format.test.ts: regression coverage in CEST / PST / UTC. 14 tests pass.
2026-05-07 23:07:26 +02:00
m
024841129f feat(t-paliad-151) shim: scripts/paliadin-shim
Server-side RPC for paliad's remote-tmux turns. Invoked via mRiver's
~/.ssh/authorized_keys command= restriction; dispatches on the verb in
$SSH_ORIGINAL_COMMAND. Four verbs: health, bootstrap, run-turn, reset.

Per the design (§5.4), this is the single SSH entry point for paliad-prod
on mLake. The Go service in cmd/server/main.go later constructs
RemotePaliadinService with this script as the only command the
authorized_keys entry permits.

Multi-character payloads (system prompt, user message) are base64-encoded
by the caller so they never have to be quoted through ssh's argv. The
shim validates UUID turn_ids, base64 decodes inputs, and never evals
$SSH_ORIGINAL_COMMAND.

Smoke-tested on mRiver:
- empty / unknown verb → exit 2 with clear stderr
- bootstrap with bad base64 → exit 2 BEFORE creating any pane
- health → "ok" on a clean tmux session

Refs m/paliad#12
2026-05-07 23:02:52 +02:00
m
db4279d148 fix(t-paliad-152): /api/events honours direct_only — Fristen/Termine subtree toggle works again
The frontend toggle on /projects/{id} Fristen + Termine emitted
`&direct_only=true`, but `handleListEvents` and `handleEventsSummary`
never read the param, so EventListFilter / EventSummaryFilter went out
without DirectOnly and the backend always returned the subtree-aggregated
default (per t-paliad-139). The toggle has been silently dead since the
Fristen/Termine surfaces migrated to /api/events in t-paliad-139.

Backend-only fix, symmetric across endpoints:

- ListFilter (deadlines), AppointmentListFilter, EventListFilter,
  EventSummaryFilter all gain DirectOnly bool.
- When ProjectID != nil && DirectOnly, the SQL predicate swaps from
  projectDescendantPredicate("p") to a direct `<alias>.project_id = :project_id`
  scope on each rail (deadline list, appointment list, deadline+appointment
  bucket counts).
- Handlers parse `direct_only` via the existing parseDirectOnly helper.
- Test extends project_filter_descendants_test.go with three DirectOnly=true
  assertions (events, deadlines, appointments) — each must collapse to the
  one direct seed row.

DirectOnly is a no-op when ProjectID is nil or PersonalOnly is set —
PersonalOnly already nullifies ProjectID.

Verlauf is untouched: it still uses /api/projects/{id}/events, which
already wired direct_only via projects.go:512.
2026-05-07 22:58:44 +02:00
m
552c9200bc Merge: t-paliad-149 PR 2 — /projects Cards view + drag-rearrange named layouts (migration 061 paliad.user_card_layouts + CardLayoutService + LayoutSpec validator + CardsPreview endpoint + frontend projects-cards.ts with HTML5 drag-and-drop edit mode) 2026-05-07 22:48:05 +02:00
m
befa41c00e design(t-paliad-151): Paliadin Tailscale SSH route to mRiver
Inventor design for routing Paliadin from paliad.de's Dokploy container
on mLake to mRiver via Tailscale + SSH, preserving m's Claude Code
subscription instead of paying Anthropic API tokens.

Three sub-designs covering m's four locked decisions (2026-05-07 22:35):
- network_mode: host on paliad (m overrode the sidecar recommendation;
  Phase A explicitly tests traefik compatibility under host mode)
- server-side paliadin-shim with one RPC per turn (run-turn / reset /
  health / bootstrap), authorized_keys command= restriction, from=mlake
- env-var routing trigger (PALIADIN_REMOTE_HOST) + Paliadin interface
  split: LocalPaliadinService keeps the laptop PoC, RemotePaliadinService
  shells out to ssh m@mriver paliadin-shim
- ed25519 keypair via Dokploy secret PALIADIN_SSH_PRIVATE_KEY, written
  to a chmod 600 tmpfile at startup; pinned host key via
  PALIADIN_KNOWN_HOSTS

Verified live before designing: mRiver tmux+claude present, mLake
Tailscale active and sees mRiver, paliad Dockerfile is alpine-minimal,
no authorized_keys on mRiver yet. No assumptions left from CLAUDE.md.

Includes: friendly error code mriver_unreachable extending t-paliad-150,
single-flight rate limit, security review (defence-in-depth via
command=/from= restrictions), three-phase rollout (manual proof →
Dockerfile bake → polish), file-level deliverables for the coder shift.

Inventor stops here — no code shipped. Awaiting m's go/no-go.

Refs m/paliad#12
2026-05-07 22:47:30 +02:00
m
aeeded7e21 feat(t-paliad-149) PR2 step 2/2: frontend — Cards view + drag-rearrange named layouts
Adds the Cards view-mode to /projects (third option in the segment-control
between Tree and Liste).

frontend/src/projects.tsx:
- View-mode segment gains "Karten" button
- Two new toolbars (initially display:none, surfaced by Cards mode):
  - .projects-cards-toolbar: layout dropdown + [Bearbeiten] + [Neue Ansicht]
    + "Alle Ebenen anzeigen" toggle
  - .projects-cards-edit-toolbar: density radio + grid select + rename /
    delete / set-default / discard / save buttons
- New container: .projects-cards-wrap > #projects-cards-grid

frontend/src/client/projects-cards.ts (NEW, ~640 LoC):
- Layout management: GET /api/user-card-layouts on first mount; auto-seeds
  Standard layout if empty (POST). Layout dropdown switches active layout
  in-place; show_all_levels toggle persists immediately.
- Edit mode: clones the active layout into editDraft; renders per-card
  fact list with drag handles + visibility checkboxes + count steppers
  (1..5) for next-events / recent-verlauf. HTML5 drag-and-drop reorders
  facts; title-row is forced to the first position so the server-side
  validator's invariant holds.
- New layout: prompts for a name, seeds with the current draft (or active
  layout's facts), POSTs, enters edit mode.
- Set-default / rename / delete: each maps to PATCH or DELETE; default
  cannot be deleted (server returns 409 + UI alerts).
- Card render: title row (icon + link + pin star), type/status chips,
  client-matter, parent-path-as-reference (parent breadcrumb deferred —
  needs an extra fetch per card), deadline-counts (subtree-aggregated
  when available), next-events from /api/projects/cards-preview, recent-
  verlauf, team-chips initials with overflow count.
- Pin click on a card star does optimistic toggle + POST/DELETE pin
  endpoint and updates treeCache in place.
- Cards sort: pinned first, then last_activity_at DESC, then title ASC.
- "Alle Ebenen anzeigen" toggle decides whether Mandanten + Litigations
  appear as their own cards (off by default — leaf-ish projects only:
  Cases, Patents, Verfahren, Projekte).

frontend/src/client/projects.ts (orchestrator):
- ViewMode type expands to "tree" | "cards" | "flat"
- View segment-control wires through to Cards mode
- render() dispatches to renderCardsView / teardownCardsView based on
  active mode

frontend/src/client/i18n.ts: 53 new keys DE+EN under projects.cards.* —
section titles, empty-states, layout picker labels (label/new/edit/save/
discard/set_default/delete/rename/is_default/new.prompt/delete.confirm/
delete.default_blocked), per-fact labels (title-row/type-chip/status-chip/
client-matter/parent-path/deadline-counts/next-events/recent-verlauf/
team-chips/reference/last-activity-at), density values (compact/roomy),
grid values (auto/2/3/4), event-kind labels (deadline/appointment/
project_event), edit toggles (toggle.hide/show/move_up/move_down/count).

frontend/src/styles/global.css: ~290 LoC appended for cards toolbar +
grid + card layout (title row / row / section / event row / team chips)
+ edit-mode chrome (drag handles, drop targets, count steppers) + dark-
themed dashed border on edit cards. Mobile media query forces single-
column grid.

i18n codegen: 1830 → 1882 keys (+52). bun run build clean. tsc on new
files clean (pre-existing JSX-IntrinsicElements noise unrelated).
go build/vet/test still clean.
2026-05-07 22:46:26 +02:00
m
4e1d311a9c feat(t-paliad-149) PR2 step 1/3: backend — migration 061 + CardLayoutService + CardsPreview
Migration 061 (paliad.user_card_layouts): per-user named card layouts.
- Partial unique index on (user_id) WHERE is_default=true keeps "at most
  one default per user" honest at the DB level.
- UNIQUE (user_id, name) so the layout dropdown can use names as stable
  labels.
- RLS owner-only (mirrors paliad.user_views from t-144).

LayoutSpec (internal/services/layout_spec.go): structured JSON validator
with KnownFactKeys registry (11 fact keys: title-row, type-chip, status-
chip, client-matter, parent-path, deadline-counts, next-events, recent-
verlauf, team-chips, reference, last-activity-at). Validator enforces:
- title-row must be the first VISIBLE fact (always-on, structural)
- no duplicate keys
- count ∈ [1, 5] only on next-events / recent-verlauf
- density ∈ {compact, roomy} (CardDensity, distinct from t-144's
  ListDensity which only ranges over comfortable/compact)
- grid_columns ∈ {auto, 2, 3, 4}

DefaultLayoutSpec returns the m-locked rich content set per design §5b.4
(9 facts, roomy density, auto grid, leaf-ish projects only).

CardLayoutService: CRUD with auto-seed (GetDefault creates "Standard"
on first call) + tx-flip-default (setting is_default=true on B clears
A in the same transaction) + ErrUserCardLayoutDefaultGate (deleting
the active default returns 409). isPgUniqueViolation maps the partial
unique index conflict to ErrUserCardLayoutNameTaken.

ProjectService.CardsPreview: per-project event rollups for the Cards view.
4 source SQLs with ROW_NUMBER() OVER PARTITION BY project_id (top 3 each
for upcoming deadlines, upcoming appointments, recent project_events) +
team-chips JOIN. Single round-trip per source, visibility-gated. Returns
map[uuid.UUID]*ProjectCardPreview with last_activity_at computed across
all sources for the orchestrator's card-grid sort.

Handlers: 5 /api/user-card-layouts/* endpoints (GET list, POST create,
PATCH update, DELETE, POST set-default) + GET /api/projects/cards-preview
(narrowable via ?ids=<csv>).

Wired in handlers.go (Services struct + dbServices struct) and
cmd/server/main.go. ErrUserCardLayoutNameTaken / NotFound / DefaultGate
mapped to 409 / 404 / 409 respectively.

Tests:
- layout_spec_test.go (8 cases, pure-Go): valid default, empty rejection,
  title-row-first invariant, hidden leading allowed, dup-key rejection,
  unknown-key rejection, count-bounds + count-on-wrong-key, density/grid
  enum, ParseLayoutSpec round-trip.
- card_layout_service_test.go (6 cases, live-DB): GetDefault auto-seeds
  + idempotent, first Create auto-becomes default, SetDefault clears
  prior, Delete refuses active default, Delete non-default works,
  duplicate name rejected, Update round-trips layout JSON.

go build / vet / test (short) clean.

Design: docs/design-projects-page-2026-05-07.md §5b.3, §5b.5, §8.2.
2026-05-07 22:41:18 +02:00
m
1061685981 Merge: t-paliad-149 PR 1 — /projects redesign tree+chips+pin+search (migration 060 paliad.user_pinned_projects + PinService + BuildTreeWithOptions + last-view restore) 2026-05-07 22:30:37 +02:00
m
a5f7b5009b feat(t-paliad-149) PR1 step 2/3: frontend rewrite — chips + pin star + last-view restore
frontend/src/projects.tsx — strip the legacy 3-select toolbar; replace with
search input + view-mode segment-control (Tree | Liste) + chip filter row
(Alle / Nur meine / Angepinnt / Status / Typ / Mit aktiven Fristen). Tree
container is the default visible mount; flat-table hidden until view mode
toggles.

frontend/src/client/projects.ts — orchestrator. Owns chip + search + view-
mode state. Last-viewed restore from sessionStorage (Q1 lock-in), URL params
override on load, syncURL on every state change. Debounced search (250ms).
Multi-select panels via <details> for status/type. Delegates rendering to
project-tree.ts (tree mode) or projects-flat.ts (flat mode).

frontend/src/client/projects-flat.ts (NEW) — extracted table render from the
old projects.ts so the orchestrator can mount/unmount cleanly.

frontend/src/client/project-tree.ts — extends ProjectTreeNode shape with
pinned, inherited_visibility, match_kind, *_subtree fields. Renders pin
star button (always-visible per design §4.6 — touch-friendly), greyed-
ancestor opacity for InheritedVisibility=true, lime backdrop on
match_kind=self. Pin click does optimistic toggle + POST/DELETE
/api/projects/{id}/pin then invalidates the tree cache.

frontend/src/styles/global.css — toolbar + chips + pin star + greyed-
ancestor + match highlighting. ~200 LoC appended.

frontend/src/client/i18n.ts — 29 new keys DE+EN under projects.toolbar.*,
projects.chip.*, projects.tree.deadlines.*, projects.tree.pin/unpin,
projects.search.match.*, projects.empty.filtered.action.

internal/services/pin_service_test.go (NEW) — live-DB tests for PinService
(pin/unpin/idempotent/owner-scope/visibility-gate) + 2 BuildTreeWithOptions
cases (PinnedSet surfaces, ScopeMine greys ancestors). Skips without
TEST_DATABASE_URL; pure-Go path runs clean.

Frontend bun build clean. go build / vet / test (short) clean.
2026-05-07 22:29:39 +02:00
m
b59e44616d Merge: t-paliad-150 — Paliadin chat fixes (bubble alignment + dark-mode contrast tokens + friendly tmux-unavailable message) 2026-05-07 22:22:41 +02:00
m
fb608321ca Merge: i18n — Sicht → Ansicht across custom views 2026-05-07 22:22:33 +02:00
m
35f307d61d fix(i18n): Sicht → Ansicht (custom views) — m's call: View is always Ansicht. Sweep applied to /views + /views/editor + sidebar + onboarding strings. Approval-context 'Sicht' (visibility-tier in derived team) left as-is — different semantic. 2026-05-07 22:22:33 +02:00
m
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.
2026-05-07 22:21:45 +02:00
m
2201c6da73 fix(t-paliad-150): Paliadin chat — bubble alignment + dark-mode contrast + friendly tmux-unavailable error
Three visual bugs from the t-146 PoC ship.

1. Bubble alignment robustness — keep `align-self: flex-end/-start`
   but also pin with `margin-left/right: auto`. align-self was already
   correct in CSS, but layered margin-auto makes the alignment
   bulletproof against any future cross-axis override.

2. Dark-mode contrast — paliadin CSS used three undefined tokens
   (`--color-accent-tint`, `--color-status-red`, `--color-status-red-tint`,
   `--color-surface-hover`) whose hardcoded fallbacks (`#e8fbb2`, `#fee`,
   etc.) always fired. In dark mode the user bubble rendered light cream
   text on light-lime background, the error bubble light cream on light
   pink — both unreadable. Repointed to the project's actual tokens:
   `--color-bg-lime-tint` (defined in both modes), `--status-red-fg/bg/border`
   (defined in both modes), `--color-surface-2` for the starter hover.
   Added explicit `color: var(--color-text)` to `.paliadin-bubble` and
   `color: var(--status-red-fg)` to the error variant. Same root cause as
   t-paliad-144's contrast sweeps (cf. memory `paliad: undefined --color-bg-muted token`).

3. Friendly tmux-unavailable error — Dokploy container has no tmux/claude
   CLI per CLAUDE.md, so prod hits `event: error` with
   `{"code":"tmux_unavailable", ...}`. The client used to dump the raw
   JSON into the bubble. Now `friendlyErrorMessage()` parses the payload
   and shows a localised "Paliadin läuft nur lokal" notice (DE+EN), with
   a `connection_lost` fallback for native EventSource transport errors
   (no `data`) or anything we don't recognise. Same code path also
   replaces the generic "Fehler beim Senden: …" pre-SSE catch block with
   `paliadin.error.upstream` so transport errors don't leak `String(err)`
   into the UI either.
2026-05-07 22:21:27 +02:00
m
438e73fd13 docs(t-paliad-149): renumber migrations 058→060 (PR 1) and 059→061 (PR 2)
058 = paliadin_poc (t-146), 059 = profession_vs_responsibility (t-148), both shipped on main 2026-05-07. Next available is 060.

Per maria's coder-shift instruction.
2026-05-07 22:15:22 +02:00
m
597d76e21c Merge remote-tracking branch 'origin/main' into mai/godel/inventor-projects-page 2026-05-07 22:14:28 +02:00
m
8bdebe9bc1 Merge: landing page text — Patent Litigation + Administration/Knowledge/Tools 2026-05-07 22:11:37 +02:00
m
d53cc3553c fix: landing page text reflects current product — 'Patent Knowledge' → 'Patent Litigation'; 'Leitfäden, Vorlagen und Dokumente' → 'Administration, Knowledge und Tools' (DE+EN) 2026-05-07 22:11:37 +02:00
m
b9824dd86f docs(t-paliad-149): lock 4 surfaced questions per m's AskUserQuestion answers
m's locks (2026-05-07 22:08):
- Q1 default landing → last-viewed restore (sessionStorage; URL params override; first-visit fallback Tree+Alle+top-level)
- New-Q20 cards default content → rich (~9 facts: title+type+status+clientmatter+parent path+deadline counts+next 3+last 3+team)
- New-Q21 cards customisation → FULL drag-rearrange + named layouts (new paliad.user_card_layouts table, migration 059)
- Q13 search shape → both (in-place page filter on active view + global Cmd-K palette unchanged)

Implementation impact: PR 2 grows from ~700-900 LoC to ~1300-1700 LoC because
m chose (c) full drag-rearrange over (b) localStorage-only. New backend
service CardLayoutService + JSON validator + 5 endpoints; frontend gets
edit-mode chrome + HTML5 drag-and-drop + layout dropdown. Optional internal
PR 2a / PR 2b split if review feels heavy (read-only cards first, then
customisation).

PR 1 (tree + chips + pin + search) is unchanged at ~1100-1400 LoC.
Other 17 recommendations stay READY-FOR-REVIEW per the dogma.
2026-05-07 22:11:18 +02:00
m
397a9b1854 docs(t-paliad-149): inventor design — projects page redesign (tree-first + chips + pinning + cards)
Three view modes (Tree default | Cards | Flat), chip filter row, pinning,
search-as-tree-filter, mobile drill-in. Cards view (m's addition) has
configurable content + per-user prefs in localStorage v1.

Q15 decision (delegated to inventor): bespoke /projects, NOT Custom Views.
Custom Views is event-shaped; projects are scope, not events. Adding
SourceProject + ShapeTree to t-144's substrate would break shape ⊥ source
orthogonality. Reversible if a unifying abstraction emerges.

Two-PR phasing: PR 1 = tree + chips + pin + search (~1100-1400 LoC,
migration 058). PR 2 = Cards view + customisation modal + cards-preview
endpoint (~700-900 LoC, additive on PR 1).

4 surfaced questions for m via AskUserQuestion: default landing + view mode,
cards default content, cards customisation scope, search shape. Other 17
questions answered with recommendations + rationale per the dogma (make it
easy for m).

Awaiting m's go on §12 questions before locking. NO coder shift until lock.
2026-05-07 22:05:44 +02:00
m
f4aa2033f9 Merge: t-paliad-148 — split project_teams.role into firm-level profession + project-level responsibility (migration 059 + ApprovalService tuple-with-gate ladder + 3-col team table + admin-team profession + onboarding picker) 2026-05-07 22:00:57 +02:00
m
efaa7787af Merge remote-tracking branch 'origin/main' into mai/kepler/inventor-profession-vs 2026-05-07 22:00:26 +02:00
m
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) 2026-05-07 22:00:26 +02:00
m
fc7192c115 Merge: t-paliad-146 — Paliadin PoC (tmux-Claude in-app AI buddy, m-only)
Phase 0 PoC of the Paliadin design (docs/design-paliadin-2026-05-07.md
§0.5). m-only via in-code email gate (services.PaliadinOwnerEmail);
no deploy-time toggle. tmux-Claude pattern lifted from goldi/mVoice
(mVoice/server.py:250-380). Migration 058 introduces
paliad.paliadin_turns audit table (full prompt+response stored at
PoC scope; production v1 swaps to hash-only). 7 unit tests on the
trailer parser / chip counter / sanitiser, all green.

Surface: /paliadin chat panel (sidebar entry under Übersicht,
revealed by /api/me on owner) + /admin/paliadin monitoring dashboard
(daily counts, classifier histogram, tool-use rate, top prompts,
recent turns). Citation chips parsed from inline marker syntax;
tool-use evidence visible under each bubble.

Production safety: routes register everywhere but the per-request
owner gate returns 404 for any user other than m. paliad.de prod
container has no tmux/claude CLI, so even m hitting the route from
there gets "tmux unavailable" — clear failure, no security surface.

Branch: mai/noether/inventor-paliadin-in-app (8d714dd).
2026-05-07 21:57:49 +02:00
m
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.
2026-05-07 21:57:20 +02:00
m
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).
2026-05-07 21:57:17 +02:00
m
2af4bf1f88 feat(t-paliad-148) commit 5/6: frontend — team-add dropdown + 3-col team table + admin-team profession + onboarding
projects-detail.tsx (the bug surface):
- Team-add dropdown switches from 7 mixed values (lead/associate/pa/of_counsel/local_counsel/expert/observer) to 4 responsibility-only values (lead/member/observer/external). Default 'member'. Closes m's bug — staffing a person no longer pretends to define their firm tier.
- Team table gains a Profession column (between Name and Responsibility), so the firm-tier badge is glanceable at staffing time.
- form.team-profession-hint surfaces the picked person's profession or warns when none is set ("kann keine 4-Augen-Genehmigungen erteilen").

projects-detail.ts:
- ProjectTeamMember type gains responsibility + user_profession. Legacy .role field kept readable for the deprecation window but UI no longer uses it.
- renderTeam renders 3-column tabular layout. Profession pill is read-only (.projekt-team-profession[--none]); responsibility is visible inline (inline-edit deferred to follow-up).
- canManagePartnerUnits switches from m.role==="lead" to m.responsibility==="lead".
- Team-add submit posts {responsibility} instead of {role}.

admin-team.tsx + client/admin-team.ts:
- New Profession column with inline-edit dropdown (6 values + "(extern)" NULL option). User type extends with profession?: string|null.
- Read-only cell uses .projekt-team-profession pill with "(extern)" placeholder for NULL.

onboarding.tsx + client/onboarding.ts:
- New required profession <select> with default 'associate'. Six values match the new enum. Hint copy explains the difference from job_title.
- POST /api/onboarding payload gains profession field.

i18n.ts: ~30 new keys DE+EN — projects.team.profession.* / .responsibility.* / projects.detail.team.col.profession / .responsibility / .form.responsibility / .form.profession.* / admin.team.col.profession.* / onboarding.profession.* / projects.team.profession.none + .hint variants.

CSS:
- .projekt-team-profession pill (firm-tier, read-only).
- .projekt-team-profession--none italic-dashed for NULL professions.
- .projekt-team-responsibility pill (per-project).
- .form-hint--warning for the team-add no-profession warning.

Build: bun build.ts clean (1723 i18n keys, all referenced). go build + go vet + go test (pure-Go) clean.
2026-05-07 21:56:18 +02:00
m
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.
2026-05-07 21:50:31 +02:00
m
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.
2026-05-07 21:49:33 +02:00
m
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.
2026-05-07 21:48:38 +02:00
m
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.
2026-05-07 21:44:14 +02:00
m
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.
2026-05-07 21:39:56 +02:00
m
8cc8435d2e Merge: fix Custom Views toast respecting its hidden attribute 2026-05-07 21:17:57 +02:00
m
c81ca6a12a merge: main into mai/noether/inventor-paliadin-in-app — pick up fritz's 057_email_broadcasts before adding 058_paliadin_poc 2026-05-07 21:17:57 +02:00
m
0f835b6c59 fix(t-paliad-144): empty Custom Views toast — .views-toast { display: flex } was overriding the [hidden] attribute (same-specificity tie, declaration order wins). Add explicit .views-toast[hidden] { display: none } so the toast respects its own hidden state. Was rendering as a visible empty box on every page load. 2026-05-07 21:17:57 +02:00
m
905e743281 Merge: fix Custom Views toast hardcoded light colors 2026-05-07 21:16:06 +02:00
m
215a1ceeda fix(t-paliad-144): Custom Views toast — replace hardcoded light-yellow (#fff8db / #f3d27a / #5b4304) with paliad tokens (lime-tint bg + border-strong + text). Was theme-blind, stayed bright in dark mode. 2026-05-07 21:16:06 +02:00
m
e4adc39833 Merge: t-paliad-147 — bulk team email (migration 057 + BroadcastService + /team filter+compose modal + /admin/broadcasts viewer) 2026-05-07 21:01:12 +02:00
m
3dffce7a0d Merge: fix Custom Views dark-mode contrast (token name mismatch) 2026-05-07 21:00:51 +02:00
m
d8b84d0c58 fix(t-paliad-144): Custom Views CSS — replace bare-name tokens (--surface, --text-muted, --border-subtle, --surface-subtle, --surface-hover) with paliad's actual --color-* tokens. The bare names don't exist in paliad's design system; the hardcoded fallbacks (#fff, rgba(0,0,0,...)) fired in dark mode → light text on white card bg. Fixes contrast on /views list+cards+calendar shapes. 2026-05-07 21:00:51 +02:00