d3aade5aac2aee019f6d71d6a8054160abc46ed1
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 2ed0ef3177 |
feat(team-admin): t-paliad-223 Slice A — Project Admin role + inheritable role-edit gate
#48 — adds 'admin' as fifth project_teams.responsibility value, plumbs an inheritable role-edit gate via the materialised ltree path. - migration 110: ALTER responsibility CHECK, CREATE paliad.effective_project_admin(uuid,uuid) STABLE SECURITY DEFINER (mirrors can_see_project shape), REPLACE project_teams_update / _insert / _delete RLS policies. Idempotent + down-mig provided. Dry-run BEGIN..ROLLBACK clean on live supabase. - services/approval_levels.go: ResponsibilityAdmin const + IsValidResponsibility extension. responsibilityOpensGate UNCHANGED — admin is orthogonal to the 4-Augen approval gate. - services/team_service.go: ChangeResponsibility() with last-admin guard inside tx (counts admins on project + ancestor chain, excludes the row being changed). RemoveMember() also runs the guard when removing an admin row. New IsEffectiveProjectAdmin() driving the frontend affordance. legacyRoleFromResponsibility: admin → 'lead' (deprecated shadow column). - services/project_service.go: ErrLastProjectAdmin sentinel mapped to 409 in writeServiceError. - handlers/teams.go: new PATCH /api/projects/{id}/team/{user_id}. RLS-enforced; non-admins get 404 to avoid existence leakage. - handlers/projects.go: GET /api/projects/{id} now wraps the payload with effective_admin bool so the frontend drives the inline-select affordance without a second round-trip. - frontend/src/projects-detail.tsx + client/projects-detail.ts: admin appears as 5th option in 'Mitglied hinzufügen' dropdown. Team-list Rolle cell switches to an inline <select> for callers with effective_admin (read-only span otherwise). Optimistic PATCH with rollback on error (last-admin guard / 403 from RLS / etc.) surfaced as transient toast in #team-msg. - i18n: +6 keys (admin label + admin.hint + 3 error toasts × 2 langs). - tests: TestIsValidResponsibility now covers admin; new TestLegacyRoleFromResponsibility pins the mapping table. go build && go test -short ./internal/... && bun run build all clean. |
|||
|
|
efaa7787af | Merge remote-tracking branch 'origin/main' into mai/kepler/inventor-profession-vs | ||
|
|
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.
|
||
|
|
52ee319fd8 |
feat(t-paliad-147): bulk team email — send to filtered selection from /team page
Implements issue #7. Adds an "E-Mail an Auswahl" button on /team that sends personalised emails to a filter-narrowed subset of the team. Each recipient gets their own envelope (per-recipient privacy, no shared To: list); From stays on the SMTP infrastructure address with Reply-To set to the human sender so replies route correctly without forging DKIM/SPF. Backend - Migration 057: paliad.email_broadcasts (subject, body, sender_id, template_key, recipient_filter jsonb, recipient_user_ids uuid[], send_report jsonb, sent_at). RLS: senders read own rows, global_admin reads all; inserts must self-attribute. No CHECK-constraint extension to partner_unit_events — broadcasts get their own table per the lock. - BroadcastService (internal/services/broadcast_service.go): validates subject/body/recipient cap (100), enforces project_lead-OR-global_admin, persists audit row, dispatches via 5-deep goroutine pool with 15s per-send timeout. Send report (sent/failed counts + per-recipient errors) is captured back into email_broadcasts.send_report. - markdown.go: minimal Markdown→safe HTML renderer (paragraphs, **bold**, *italic*, `code`, [text](url), bullet lists). Inputs are HTML-escaped first; only whitelisted tags re-emitted. Script tags and javascript: URLs can't slip through. - Placeholder substitution: {{name}}, {{first_name}}, {{role_on_project}} (whitespace tolerated). Unknown {{...}} tokens pass through unchanged. - mail_service.go: buildMIMEWithReplyTo helper layers a Reply-To header on top of the existing multipart/alternative envelope. - TeamService.ListMembershipsIndex: visibility-gated user→project_ids index. Powers the /team project multi-select filter without N round trips per project. - Handlers: POST /api/team/broadcast (gateOnboarded; service enforces authority), GET /api/team/memberships, GET /api/admin/broadcasts (list), GET /api/admin/broadcasts/{id} (detail), GET /admin/broadcasts (page). /admin/broadcasts is gateOnboarded (not adminGate) so leads can see their own sends; the service applies the per-row visibility filter. Frontend - /team gains a project multi-select chip dropdown (visible projects loaded from /api/projects, intersected against the memberships index) alongside the existing office and role filters. - "E-Mail an Auswahl (N)" button appears only when canBroadcast() is true (global_admin always; non-admin needs lead-ship on selected projects, or at least one project when no filter is set). Server still re-checks per send. - Compose modal (broadcast.ts): subject + body textarea + optional template dropdown (loads existing email templates and strips Go-template directives) + recipient preview (first 5 + expand) + send. Hard-blocks empty subject/body and N=0. Shows per-send report on success. - /admin/broadcasts viewer: read-only list with click-row-to-expand detail (subject, body, recipient list, send_report counts). Tests - broadcast_service_test.go: placeholder substitution table-driven, Markdown safe-render incl. XSS guards (<script>, javascript: URLs), validation cases (empty subject/body, recipient cap, invalid email), signature rendering DE/EN. - broadcast_service_live_test.go: end-to-end Send + List + Get + visibility rules (lead can send on own project, member cannot, admin sees all, member can't read lead's row). Skips when TEST_DATABASE_URL is unset. i18n: 60 new keys × 2 langs (broadcast modal labels, error messages, recipient summary, /admin/broadcasts viewer, common.close/loading/forbidden/ load_error). |
||
|
|
460736ad1e |
refactor(t-paliad-092): rename Go module path patholo → paliad
F-6 from t-paliad-074 architecture audit. The Gitea repo was renamed m/patholo → mAi/paliad → m/paliad, but go.mod still declared `mgit.msbls.de/m/patholo` and every internal import echoed the pre-rebrand name. Sweep: - go.mod: module path → mgit.msbls.de/m/paliad - All *.go files: imports rewritten via sed - README.md, docs/design-kanzlai-integration.md: mAi/paliad → m/paliad - Frontend issue-reference comments (mAi/paliad#N → m/paliad#N) in i18n.ts, theme.ts, sidebar.ts, app.ts, Sidebar.tsx, PWAHead.tsx, global.css Verified: go build/vet/test ./... clean, bun run build clean, no remaining mgit.msbls.de/m/patholo or mAi/paliad references outside docs that intentionally describe the rename history. |
||
|
|
ce3227c1c0 |
refactor(t-paliad-080): service-layer naming sweep — Notiz/Termin/Frist/Projekt/Partei → Note/Appointment/Deadline/Project/Party
Mechanical rename across 8 service files plus their handler call sites and two related helpers. The English types existed already; what changed are the input-struct names, helper functions, list/create method suffixes, and parameter names so they no longer mix English types with German parameter names. Renames cover: - CreateNotizInput/UpdateNotizInput → CreateNoteInput/UpdateNoteInput, notizColumns/notizSelect → noteColumns/noteSelect, ListForProjekt/Frist/ Termin → ListForProject/Deadline/Appointment, CreateForProjekt/Frist/ Termin → CreateForProject/Deadline/Appointment, fristProjectID → deadlineProjectID - CreateTerminInput/UpdateTerminInput → CreateAppointmentInput/ UpdateAppointmentInput, terminColumns → appointmentColumns, ListForProjekt → ListForProject; parameter renames terminID → appointmentID, projektID → projectID - CreateFristInput/UpdateFristInput → CreateDeadlineInput/ UpdateDeadlineInput, fristColumns → deadlineColumns, ListForProjekt → ListForProject, isValidFristStatus → isValidDeadlineStatus; parameter renames fristID → deadlineID, projektID → projectID - CreateProjektInput/UpdateProjektInput → CreateProjectInput/ UpdateProjectInput, projektColumns → projectColumns, validateProjektStatus → validateProjectStatus, ProjektRole comment → ProjectRole - CreateParteiInput → CreatePartyInput, parteiColumns → partyColumns, ListForProjekt → ListForProject; parameter renames parteiID → partyID - OnTerminCreated/Updated/Deleted → OnAppointmentCreated/Updated/Deleted on the AppointmentCalDAVPusher interface and its CalDAVService impl - formatTermin → formatAppointment in caldav_ical - ListForProjekt → ListForProject, listWithProjekt → listWithProject, checklistInstanceWithProjektSelect → checklistInstanceWithProjectSelect, ClearProjekt → ClearProject (JSON tag clear_projekt unchanged — wire format) - insertProjectEvent helper parameter projektID → projectID, error message "insert projekt_event" → "insert project_event" - TeamService AddMember/RemoveMember/ListDirectMembers/ListEffectiveMembers parameter projektID → projectID; matching handler renames - Frontend doc-comments referencing CreateProjektInput/UpdateProjektInput updated to CreateProjectInput/UpdateProjectInput JSON wire tags (clear_projekt, etc.) and German user-facing strings (glossary entries, search.go labels, email templates, changelog, Terminsgebuehr, Fristenrechner product name) are intentionally untouched. API contract unchanged. go build/vet/test ./... clean. Frontend bun build clean. |
||
|
|
31db66e3b7 |
refactor(t-paliad-076): consolidate visibility predicate — 6 dashboard/agenda sites use helper
F-2 from t-paliad-074 audit. The inlined visibility predicate had drifted
back into 6 hot-path SQL sites despite the central helper extracted in
t-paliad-058. Consolidating now so future visibility changes (e.g.
Chinese-wall in design v2 §8) only need one edit.
**Sites converted (6):**
- dashboard_service.go:158, 214, 244, 274
- agenda_service.go:138, 204
All six replace `$N = 'global_admin' OR EXISTS (path-walk)` with the
existing `visibilityPredicatePositional("p", 1)` helper. The helper
resolves global_admin via EXISTS on paliad.users — the role string no
longer flows through positional args, removing one foot-gun (typo'd
literal mismatched against bound role) entirely. Equivalence verified
on the live youpc DB:
tester@hlc.de (global_admin, 1 team membership):
old predicate count = 11 new predicate count = 11
standard user (no team):
old predicate count = 0 new predicate count = 0
**No new helper variant added.** The audit suggested
`visibilityPredicateLateral`, but the existing positional helper drops
into the dashboard/agenda WHERE clauses unchanged — adding a redundant
variant would be technical debt. dashboard/agenda do not use LATERAL
JOIN; they use plain WHERE EXISTS in (sub-)SELECT context, which is
already what visibilityPredicatePositional emits.
**Other 4 sites flagged by audit — left intentionally:**
- reminder_service.go:312, 325 are role-restricted (`pt.role = 'lead'`)
membership checks, NOT visibility predicates. Adding a global_admin
shortcut to the lead branch would over-include rows: every global
admin would receive every project's lead-targeted reminder, even with
the `own.escalation_contact_id` override that exists precisely to
avoid that. global_admin already has its own dedicated branch in the
query (`$3 = TRUE AND own.escalation_contact_id IS NULL` at line 328).
- deadline_service.go:422 (`assertCanAdminProject`) is role-restricted
(`pt.role IN ('admin', 'lead')`) and already short-circuits global_admin
at the Go level before the SQL runs (line 413). Both halves correct;
no change needed.
- team_service.go:162 (`IsEffectiveMember`) was dead code with no callers
in the entire repo. "Is this user a structural team member?" and
"can this user see this project?" are different questions; adding a
global_admin shortcut would have conflated them. Deleted instead.
**Test:** new TestVisibilityPredicate_DashboardAgendaForGlobalAdmin in
visibility_test.go seeds a project + deadline + appointment + activity
event with project_teams empty, then asserts a global_admin sees all
four on /dashboard and /agenda while a standard user sees none. Skips
when TEST_DATABASE_URL is unset (matching the existing live-DB tests).
**Pre-existing finding (separate concern):** the live-DB test gate is
currently blocked locally by a stale `public.paliad_schema_migrations`
(version=2, dirty=t) left over from before the schema-pinned tracker
landed. Authoritative `paliad.paliad_schema_migrations` is at version
27, dirty=f. Out of scope for this task; should be filed as cleanup.
|
||
|
|
70c3f08668 |
fix(projects-detail, services): empty-list endpoints returned JSON null → tab content blank
m reported /projects/{id} loaded the chrome and tabs but every panel was
empty even with deadlines/appointments/team rows that should render.
Console error: "Cannot read properties of null (reading 'length')" at
projects-detail.js — the Project Detail page expects every list endpoint
to return [] but at least two were returning literal JSON null.
Reproduced via the in-page fetch console:
/api/projects/{id}/parties → 200, body: "null"
/api/projects/{id}/children → 200, body: "null"
/api/projects/{id}/deadlines → 200, body: "[…]" (had data, fine)
/api/projects/{id}/team → 200, body: "[…]" (had data, fine)
Root cause: every list service in internal/services declared its result
as `var rows []models.X` and returned that to the handler, which
encoding/json marshals as `null` when the SELECT returns zero rows
(nil slice, not empty slice). Most endpoints happen to have data so
the bug stayed dormant until t-paliad-038 hit /projects/{id} where
parties + children are commonly empty.
Fix at the source — every list service that JSON-marshals to a client
now initialises `rows := []models.X{}` so the encoder produces `[]`:
party_service ListForProjekt
project_service List, ListAncestors, BuildTree, GetTree
(ListChildren goes through List)
deadline_service List + ListForProjekt
appointment_service List + ListForProjekt
note_service ListForProjekt
checklist_instance_service ListForProjekt
team_service List
department_service List + ListMembers + ListWithMembers
caldav_service was deliberately left alone — its lists are admin-only
debug surfaces, not user-facing tab fillers, and changing them would
mix scopes.
Belt-and-braces on the client too — projects-detail.ts now coerces every
`await resp.json()` for an array endpoint with `?? []` so a future
service regression can't crash the page.
Verified: go build/vet/test clean, bun run build clean.
|
||
|
|
3faec6c526 |
refactor(rename): German→English for backend (tables, types, services, handler files)
t-paliad-025 — Phase 1: backend rename.
Migrations 018+019 rewritten from scratch with English table/column
names throughout. Since v2 schema (018/019) has never been applied to
youpc prod DB, this is a clean replacement — not an ALTER RENAME chain.
Pre-existing German tables (parteien, fristen, termine, dokumente,
akten_events, notizen) are renamed inline in 018 via ALTER TABLE … RENAME
TO alongside the akte_id → project_id column rewrite.
Renames applied:
projekte → projects
projekt_teams → project_teams
projekt_events → project_events (via akten_events → project_events)
fristen → deadlines
termine → appointments
parteien → parties
notizen → notes
dezernate → departments
dezernat_mitglieder → department_members
dokumente → documents
can_see_projekt → can_see_project
notiz_is_visible → note_is_visible
akte_id / frist_id / termin_id / akten_event_id → project_id /
deadline_id / appointment_id / project_event_id
termin_type → appointment_type
Go types + services renamed:
Projekt / ProjektService / ProjektEvent / ProjektTeamMember
Frist / FristService / FristWithProjekt
Termin / TerminService / TerminWithProjekt / TerminType
Notiz / NotizService / ChecklistInstanceWithProjekt
Dezernat / DezernatService / DezernatMitglied
Partei / Parteien / ParteienService
Files renamed (git mv):
internal/services/{projekt,frist,termin,notiz,dezernat,parteien}_service.go
→ {project,deadline,appointment,note,department,party}_service.go
internal/handlers/{projekte,fristen,fristen_pages,termine,termine_pages,
notizen,dezernate,akten_pages,gerichte,glossar,checklisten}.go
→ {projects,deadlines,deadlines_pages,appointments,appointments_pages,
notes,departments,projects_pages,courts,glossary,checklists}.go
internal/checklisten/ → internal/checklists/
internal/db/migrations/018_projekte_v2.* → 018_projects_v2.*
internal/db/migrations/019_seed_dezernate_from_user_text.*
→ 019_seed_departments_from_user_text.*
User-facing i18n strings (DE/EN labels) stay untouched. Product names
Fristenrechner / Kostenrechner / Gebührentabellen stay German.
Build + vet + tests clean.
|
||
|
|
9aa8037193 |
refactor: services — Projekt, Team, Dezernat services (WIP Phase 2)
Models: Akte → Projekt (tree type + parent_id + path + client/matter numbers + netDocuments URL + type-specific client/patent/case columns). AkteEvent → ProjektEvent. FristWithAkte → FristWithProjekt. TerminWithAkte → TerminWithProjekt. Notiz.AkteID → ProjektID. ChecklistInstance.AkteID → ProjektID. Partei.AkteID → ProjektID. User adds AdditionalOffices pq.StringArray. Services: - NEW projekt_service.go replaces akte_service.go. Adds tree ops: List/GetByID/ ListChildren/ListAncestors/GetTree. Create auto-adds creator to projekt_teams role=lead in same tx. ResolveClientNumber walks path for inheritance. Visibility helpers (visibilityPredicate / Positional / Placeholder) centralise team-based access check: admin OR any ancestor/direct projekt_teams row. - NEW team_service.go — AddMember/RemoveMember/ListDirectMembers/ ListEffectiveMembers (unions direct + inherited via path, dedup by user; direct wins)/IsEffectiveMember. Inherited=true set at read time only. - NEW dezernat_service.go — admin-gated CRUD + member add/remove + user membership lookup for settings page. - frist_service.go → projekt_id everywhere, uses visibilityPredicate. ListFilter. AkteID → ProjektID. - termin_service.go → projekt_id everywhere. CalDAV log reads projekt_events. - notiz_service.go → projekt_id polymorphic branch; eventProjektID() looks at projekt_events; akten_event_id column kept (FK now resolves to projekt_events). - parteien_service.go → projekt_id. - checklist_instance_service.go → projekt_id with ClearProjekt flag. - dashboard_service.go → rewrites all four queries against projekte + projekt_events + projekt_teams. Matter/Upcoming/Activity surfaces use ProjektID/ProjektTitle/ProjektRef. - reminder_service.go → joins paliad.projekte, aliases a.reference AS akte_aktenzeichen for template compat. Handlers/tests still reference old API — Phase 2 completion requires handler rewrite (next commit). Build currently broken in internal/handlers. |