# Paliad Authenticated Smoke Test — 2026-04-25

**Target:** https://paliad.de
**Run by:** paliad/curie (t-paliad-034)
**Method:** Playwright headless (Chromium 1.59.1, viewport 1280×900) via `/mai-tester`
**Auth:** logged in as `tester@hlc.de` (UUID `591183c3-ca3a-4939-acac-5953c1dd08d8`, role=`admin`, office=`munich`, lang=`de`)
**Screenshots:** `tests/screenshots-auth-2026-04-25/auth-NN-*.png`
**Trigger:** follow-up to t-paliad-033 — exercise the gated surface, validate the three features that shipped today.

## Headline

Login works. **Critical regression: every endpoint that lists Projects/Deadlines/Appointments returns HTTP 500 for the test admin.** Knowledge tools, Team directory, Settings, Glossary, Courts and the public landing all render cleanly. Project tree (`/api/projects/tree`) works in isolation; flat `/api/projects` does not, breaking `/projects`, `/projects/new` (POST), `/deadlines`, `/appointments` and the project-detail page render. **Reminder service could not be exercised** — every prerequisite (create project, list deadlines) is broken end-to-end.

Two new features (Project tree, Team directory) ship in a partial state; reminder service is unverifiable from the UI.

## Setup note (resolved before the smoke run)

The first-attempt seed of the `tester@hlc.de` admin landed in the wrong Postgres (the Supabase MCP I had access to → `supa.flexsiebels.de`, not Paliad's actual auth DB at youpc `100.99.98.201:11833`). Login returned 401 for ~6 minutes while head re-seeded via psql against the correct DB. The original seeded UUID `b62cc337-1750-4599-8e42-10764d0b9a43` was deleted from the wrong DB; the working UUID is `591183c3-ca3a-4939-acac-5953c1dd08d8`. Worth noting because future smoke tests will hit the same trap: the youpc Postgres MCP is "broken" per `~/.claude/CLAUDE.md`, so seeding has to go through psql, not MCP.

## Per-URL Results

### Core app shell

| URL | HTTP | Console | Render | Notes |
|-----|------|---------|--------|-------|
| `/login` (form) | 200 | clean | ✅ | DE default, EN toggle works, HLC hint visible |
| `POST /api/login` | 200 | clean | ✅ | Cookie set, redirects to `/dashboard` |
| `/dashboard` | 200 | 0 errs (favicon 404 only) | ⚠️ | Renders; greeting/office correct (`Test Tester`, `München`); traffic-light shows `1 Überfällig`. Activity log leaks 2/3 raw i18n keys (see Bug 4) |
| `/agenda` | 200 | clean | ✅ | Empty state for "Both / 30 days"; type/range chips work |
| `/changelog` | 200 | clean | ✅ | Renders changelog list, "What's New" header, lime accents |
| `/whatsnew` | **404** | err | ❌ | Brief expected this to work — it doesn't exist; sidebar uses `/changelog` |
| `/search?q=test` | **404** | err | ❌ | Brief expected this to work — route does not exist |
| `/settings` | 200 | clean | ✅ | Profile tab populated correctly, all 4 tabs (Profile/Notifications/CalDAV/Dezernat) render in the SPA |
| `/settings/caldav` | 200 | clean | ✅ | Direct deep-link works |
| `/settings/notifications`, `/settings/dezernat` | **404** | — | ⚠️ | Tab deep-links 404 (only `caldav` is routed); not blocking — in-page tabs work |

### The three new features

| Feature | URL | Verdict | Detail |
|---------|-----|---------|--------|
| Project tree (t-paliad-028) | `/projects` (flat **and** tree) | ❌ **broken** | Banner "Project management is currently unavailable — please contact an administrator." `/api/projects` and `/api/projects?view=tree` both return 500. The new `/api/projects/tree` endpoint **does** return 200 with valid JSON, but the page never reaches it because the initial flat-list load fails first. Tree-view dropdown is disabled implicitly by the error banner — no toggle UX possible. |
| Team directory (t-paliad-029) | `/team` | ⚠️ **partial** | Shows `Test Tester` (admin/Munich) + `Matthias` (Counsel Knowledge Lawyer / Düsseldorf, real prod data). "By office" / "By department" toggle works; office filter pills work (`Munich` filter narrows to 1/2 correctly); search by name works (`Tester` → 1/2). **But:** `GET /api/departments?include=members` returns 404/500 silently — page still renders because the data falls back to `/api/users`. The fallback works; the dept include endpoint is bogus. |
| Reminder service (t-paliad-032) | (UI prerequisite) | ❌ **unverifiable** | Cannot create a fresh deadline: `POST /api/projects` returns 500, so no project to attach a deadline to. The existing prod project (`Mandant vs Gegner`) is invisible in the project-detail page (Bug 2), so its deadline form is unreachable. SMTP / email side untested. |

### Knowledge platform (auth-gated, all green)

| URL | HTTP | Console | Visual |
|-----|------|---------|--------|
| `/tools/fristenrechner` | 200 | clean | ✅ Patent Deadline Calculator renders, UPC/German/EPO selectors visible |
| `/tools/kostenrechner` | 200 | clean | ✅ Cost Calculator renders with default 1M EUR, totals computed live (51,924.92 €) |
| `/tools/gebuehrentabellen` | 200 | clean | ✅ |
| `/checklists` | 200 | clean | ✅ |
| `/glossary` | 200 | clean | ✅ 89/89 terms; DE→EN toggle changes title to `Patentglossar` correctly |
| `/courts` | 200 | clean | ✅ 41/41 courts; UPC/Germany/EPA/National + 13 country filters |
| `/links` | 200 | clean | ✅ |
| `/downloads` | 200 | clean | ✅ |

### Aktenverwaltung core (broken)

| URL | HTTP | Failed XHRs | Notes |
|-----|------|-------------|-------|
| `/projects` | 200 (page) | `/api/projects` → 500 | "Project management is currently unavailable" |
| `/projects?view=tree` | 200 (page) | `/api/projects?view=tree` → 500 | Same banner; `view=tree` query param has no SSR effect |
| `/projects/new` | 200 (page) | `POST /api/projects` → 500 | Form renders; submit displays `internal error` inline; no project created |
| `/projects/{id}` | 200 (page) | n/a | "Project not found or no access." But `GET /api/projects/{id}` returns 200 — page-render visibility check is inconsistent with the API |
| `/projects/{id}/deadlines` | 200 (page) | n/a | Same "not found or no access" — same root cause |
| `/deadlines` | 200 (page) | `/api/deadlines?status=pending`, `/api/deadlines/summary`, `/api/projects` all 500 | "Deadline management currently unavailable" |
| `/appointments` | 200 (page) | `/api/appointments?`, `/api/appointments/summary`, `/api/projects` all 500 | "Appointments unavailable — please contact your administrator." |

### DE/EN toggle smoke

| Page | DE | EN | Verdict |
|------|----|----|---------|
| `/dashboard` | "Guten Tag, Test Tester" | "My matters / Sign Out / Internal use only" | ✅ |
| `/projects` | "Projekte" sidebar / "Akten" | "Projects / Internal use only" | ✅ (chrome) |
| `/glossary` | "Patentglossar" / "Patentglossar — Paliad" title | "Patent Glossary" | ✅ |

## Bug-hunt findings (top 10 by severity)

### 🔴 Bug 1 — `/api/projects` (GET list, POST create) returns 500

**Severity:** showstopper for Aktenverwaltung. Reproduction: `curl https://paliad.de/api/projects` with the test admin's session cookie returns `{"error":"internal error"}`. Same pattern for `POST /api/projects` (project creation) and for `/api/deadlines`, `/api/appointments`, `/api/deadlines/summary`, `/api/appointments/summary`. Likely root cause: see Bug 3.

### 🔴 Bug 2 — `/projects/{id}` SSR shows "not found" while `/api/projects/{id}` returns 200

**Severity:** breaks every project-detail navigation. Repro: `curl https://paliad.de/api/projects/b0b39a87-c67e-4fa2-8935-4511ccce80dc` → 200 with full payload; visiting `https://paliad.de/projects/b0b39a87-c67e-4fa2-8935-4511ccce80dc` in the browser → page text "Project not found or no access." The page-render handler (`handleProjectsDetailPage`, registered for 9 sub-paths in `internal/handlers/handlers.go:219-227`) uses a different visibility check than the `/api/projects/{id}` handler. Almost certainly downstream of Bug 3.

### 🔴 Bug 3 — `paliad.can_see_project()` references renamed-away tables

**Severity:** root cause for Bugs 1 + 2 + the broken `/deadlines` and `/appointments`. The DB function still queries `paliad.projekte` and `paliad.projekt_teams` (German names dropped during the rename to English):

```sql
-- excerpt from pg_get_functiondef('paliad.can_see_project')
FROM paliad.projekte  target
JOIN paliad.projekt_teams pt
  ON pt.user_id = auth.uid()
 AND pt.projekt_id = ANY(string_to_array(target.path, '.')::uuid[])
WHERE target.id = _projekt_id
```

These tables no longer exist (`SELECT EXISTS(... 'projekte') AS has_projekte` → `f`). Calling the function fails: `ERROR: relation "paliad.projekte" does not exist`. The function is called by the RLS policies on `paliad.projects` (`projekte_select USING (paliad.can_see_project(id))`), so any code path that exercises RLS — page-render handlers, anything not running as `supabase_admin`/`postgres` — errors out. The Go tree handler probably works because it uses the service-side `visibilityPredicate` SQL embedded in its own query and runs as the connection's elevated role, bypassing RLS.

**Fix:** update the function body to reference `paliad.projects` and `paliad.project_teams`. There's likely a missing migration that renamed the tables but skipped the functions/policies.

```sql
CREATE OR REPLACE FUNCTION paliad.can_see_project(_projekt_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
SET search_path TO 'paliad','public' AS $$
  SELECT EXISTS (SELECT 1 FROM paliad.users u WHERE u.id = auth.uid() AND u.role = 'admin')
      OR EXISTS (
        SELECT 1
          FROM paliad.projects target
          JOIN paliad.project_teams pt
            ON pt.user_id = auth.uid()
           AND pt.project_id = ANY(string_to_array(target.path,'.')::uuid[])
         WHERE target.id = _projekt_id);
$$;
```

The grep also turned up old-name fkey constraints (`projekte_*_fkey`, `akten_events_projekt_id_fkey`, `dokumente_projekt_id_fkey`, `fristen_projekt_id_fkey`, `notizen_projekt_id_fkey`, `parteien_projekt_id_fkey`, `projekt_teams_projekt_id_fkey`, `termine_projekt_id_fkey`) — cosmetic, but they suggest the rename migration was a `ALTER TABLE ... RENAME TO` only and never touched dependent objects. Worth a sweep.

### 🟠 Bug 4 — Dashboard activity log leaks raw i18n keys

**Severity:** cosmetic, but it's on the landing page — worst place for it. Visible strings on `/dashboard` "Letzte Aktivität":
- `Matthias dashboard.action.notiz_created 000001.000001 Notiz hinzugefügt`
- `Matthias dashboard.action.frist_created 000001.000001 Frist angelegt`
- `Matthias legte Akte an 000001.000001 Akte created` ← the third entry mixes translated DE ("legte Akte an") with raw EN ("Akte created") — likely two different fallback paths.

The keys `dashboard.action.notiz_created` and `dashboard.action.frist_created` are not in the i18n bundle for DE (and presumably not EN either). The third entry's "Akte created" suffix looks like a default English title from the audit-event row that didn't go through translation. Three separate bugs in the same widget.

### 🟠 Bug 5 — Untranslated dropdown labels in `/deadlines` and `/appointments`

**Severity:** cosmetic. The Matter filter on `/deadlines` shows `fristen.filter.project.all` as its first option text; `/appointments` shows `termine.filter.project.all`. Both are raw i18n keys. (The pages themselves are also broken — Bug 1 — but the label leak is independent.)

### 🟠 Bug 6 — `/api/departments?include=members` returns 404 / 500 inconsistently

**Severity:** low (page renders via fallback), but worth tightening. First load on `/team` produced a 500; later fetches from the page return 404. Either the route shouldn't be hit at all (front-end should stop firing it) or it should be implemented. Both observed failures are silently swallowed by the team page — visible only in the console.

### 🟠 Bug 7 — `/whatsnew` and `/search` return 404

**Severity:** medium. The brief expected both routes to work; reality is `/changelog` (named "What's New" in the UI) and no `/search` page at all. Either:
- the brief is stale (these routes were never added) — fix the brief,
- or the routes were dropped in a rename — restore or redirect.

The sidebar correctly links to `/changelog`; users who type `/whatsnew` from memory will hit a bare "404 page not found" with no chrome at all (same for `/search`).

### 🟠 Bug 8 — Tab deep-links broken on `/settings`

**Severity:** low. Tabs visible in the UI: Profile / Notifications / CalDAV / Dezernat. Direct URL `/settings/caldav` → 200 ✅. `/settings/notifications`, `/settings/dezernat` → 404. The in-page click navigation works for all four — only the deep-link form is inconsistent.

### 🟡 Bug 9 — 404 pages have no chrome

**Severity:** polish. `/whatsnew`, `/search`, `/settings/notifications` all render bare `404 page not found` text — no sidebar, no header, no link back. Existing logged-in users hitting a typo get a jarring full-page break. `/projects/{nonexistent-uuid}` correctly renders the chrome + a friendly "not found" card; the 404 handler should match that style.

### 🟡 Bug 10 — Login form preserves session on failure

**Severity:** very minor. After a failed login attempt, the prior 401 console error stays attached to the session and is later replayed when console history is requested with `all=true`. Functional impact: zero. Surface this only if you're chasing a clean console for marketing screenshots.

---

## Acceptance check

- ✅ Login successful, cookie persisted, dashboard reached
- ⚠️ Three new features exercised — Team directory ✅, Project tree ❌ (page broken), Reminder service ❌ (unverifiable, no path to create a deadline)
- ✅ Report file written
- ✅ MCP `mcp__mai__update_task(id="t-paliad-034", status="done")` will be called next
- ⏳ Commit pending

## Cleanup

Per brief: test admin (`tester@hlc.de`, UUID `591183c3-ca3a-4939-acac-5953c1dd08d8`) **left intact** in youpc Postgres; password unchanged; no rows deleted.

Side-effect to flag: while debugging the seeding mismatch, I overwrote `auth.users.encrypted_password` for the **decoy** UUID `b62cc337-1750-4599-8e42-10764d0b9a43` in `supa.flexsiebels.de` Supabase (twice, with different bcrypt hashes for the same brief password). That DB is **not** Paliad's auth DB so there is no production impact, but if anything else points at that decoy it's now in an unspecified state. Head deleted the decoy UUID before reseeding — should be fully gone.

## Recommendation

1. **Ship a hotfix migration** that rewrites `paliad.can_see_project` to use the renamed tables (Bug 3). Without it the entire admin/lead surface for projects/deadlines/appointments is unusable. Real production users may also be affected — needs verification with a non-admin login.
2. **Audit the rename migration** for other stale references: function bodies, fkey constraint names (cosmetic), trigger functions (`projekte_rewrite_subtree`, `projekte_sync_path` are still old-named — they sit on `paliad.projects` so they probably work via the trigger binding, but worth a sweep).
3. **Reconcile the brief**: `/whatsnew` and `/search` either need to exist or stop being expected.
4. **Fix the activity-log i18n keys** before the next user-facing demo (Bug 4).
5. **Wire the 404 handler into the chrome** (Bug 9) — small change, big polish gain.
