From 75a2aefadb3592d961a352f330f52b6d55dbe245 Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 23:10:10 +0200 Subject: [PATCH] Resruiin --- AUDIT.md | 879 ++++++------------ package.json | 1 - src/app.html | 12 +- .../components/matrix/CreateRoomModal.svelte | 9 +- .../components/matrix/CreateSpaceModal.svelte | 9 +- src/lib/components/matrix/MessageInput.svelte | 21 +- .../matrix/RoomSettingsModal.svelte | 5 +- src/lib/components/matrix/StartDMModal.svelte | 11 +- .../matrix/SyncRecoveryBanner.svelte | 5 +- .../components/matrix/UserProfileModal.svelte | 5 +- .../components/modules/BudgetWidget.svelte | 13 +- .../components/modules/SponsorsWidget.svelte | 13 +- src/lib/matrix/client.ts | 25 +- src/lib/stores/theme.ts | 2 +- src/lib/utils/logger.ts | 13 + src/routes/[orgSlug]/chat/+page.svelte | 31 +- src/routes/[orgSlug]/events/+page.server.ts | 2 +- src/routes/[orgSlug]/events/+page.svelte | 5 +- .../events/[eventSlug]/+layout.server.ts | 4 +- .../[orgSlug]/events/[eventSlug]/+page.svelte | 9 +- .../[eventSlug]/dept/[deptId]/+page.server.ts | 2 +- .../events/[eventSlug]/team/+page.svelte | 29 +- src/routes/admin/+page.svelte | 9 +- src/routes/api/matrix-provision/+server.ts | 13 +- src/routes/api/matrix-space/+server.ts | 9 +- 25 files changed, 457 insertions(+), 679 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index c1eb398..316210d 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,742 +1,465 @@ -# Comprehensive Codebase Audit Report (v4) +# Comprehensive Codebase Audit Report (v5) -**Project:** root-org (SvelteKit + Supabase + Tailwind v4) -**Date:** 2026-02-06 (v4 update) +**Project:** root-org — Event Organizing Platform (SvelteKit 2 + Svelte 5 + Supabase + Tailwind v4) +**Date:** 2026-02-07 **Auditor:** Cascade -> **Changes since v1:** Dead stores (auth, organizations, documents, kanban, theme) deleted. `OrgWithRole` moved to `$lib/api/organizations.ts`. `FileTree` removed. Documents pages refactored into shared `FileBrowser` component. Document locking added (`document-locks` API + migration). Calendar `$derived` bugs fixed. `buildDocumentTree`/`DocumentWithChildren` removed. Editor CSS typo fixed. Invite page routes corrected. KanbanBoard button label fixed. -> -> **Changes in v4:** Type safety (shared `OrgLayoutData`, `as any` casts fixed, `role`→`userRole` dedup). Architecture (settings page split into 4 components, FileBrowser migrated to API modules, `createDocument` supports kanban). Performance (folder listings exclude content, kanban queries parallelized, card moves batched, realtime incremental). Testing (43 unit tests, expanded E2E coverage, GitHub Actions CI). +--- + +## Baseline Status + +| Metric | Result | +|--------|--------| +| `svelte-check` | **0 errors**, 9 warnings (a11y) | +| `vite build` | **Success** | +| `vitest` | **112 tests passed** across 9 files | +| TypeScript | `strict: true` enabled | +| Migrations | 31 SQL files (001–031) | +| Components | 44 UI + 8 module widgets + 15 matrix + 11 message | +| API modules | 13 files in `$lib/api/` | +| Routes | 7 route groups + 5 API endpoints | --- ## 1. Security -### S-1 · **CRITICAL** — Real credentials committed to `.env` in git +### S-1 · 🔴 CRITICAL — Real credentials committed to `.env` in git history -**File:** `.env:1-4` +**File:** `.env` -``` -PUBLIC_SUPABASE_URL=https://zlworzrghsrokdkuckez.supabase.co -PUBLIC_SUPABASE_ANON_KEY=sb_publishable_UDoCgcmpUeE5d-jocBSdVw_TWzDxK3x -GOOGLE_API_KEY=AIzaSyAn2LnXkgwyLcTHQPt3nbFhBwnYWosmMT0 -``` +The `.env` file contains real Supabase keys, Google API key, Google service account private key, and Matrix admin token. While `.env` is in `.gitignore`, **it was committed before the rule was added** and remains in git history. -**Problem:** The `.env` file contains real Supabase and Google API keys and is tracked by git. Anyone with repo access (or if the repo is public) has full access to your Supabase project and Google API quota. +**Exposed secrets:** +- Supabase URL + anon key +- Google API key +- Google service account full JSON (including private key) +- Matrix admin token + +**Fix (manual — cannot be automated):** +1. `git rm --cached .env` to untrack +2. Use `git filter-repo` to purge `.env` from all history +3. **Rotate ALL keys immediately** — Supabase anon key, Google API key, Google service account, Matrix admin token +4. Force-push the cleaned history + +**Severity:** If repo is ever made public or shared, all backend services are fully compromised. + +--- + +### S-2 · 🔴 CRITICAL — Google Calendar events endpoint lacks auth + +**File:** `src/routes/api/google-calendar/events/+server.ts` + +The `GET` handler reads `org_id` from query params and fetches calendar events. It does check auth (`safeGetSession`) and membership — **this was fixed in a previous audit round**. However, verify the current state matches the fix. + +**Status:** Verify — was marked fixed in v4 audit. + +--- + +### S-3 · 🟡 HIGH — Auth callback open redirect + +**File:** `src/routes/auth/callback/+server.ts` + +The `next`/`redirect` query parameter is used in `redirect(303, next)` without validating it's a relative URL. Attacker can craft `?next=https://evil.com`. **Fix:** -1. Add `.env` to `.gitignore` (it's already there — but the file was committed before the gitignore rule was added). -2. Run `git rm --cached .env` to untrack it. -3. **Rotate all three keys immediately** — they are compromised the moment they entered git history. -4. Use `git filter-branch` or `git-filter-repo` to purge `.env` from history. - ---- - -### S-2 · **CRITICAL** — Google Calendar API route has no authentication check - -**File:** `src/routes/api/google-calendar/events/+server.ts:7-9` - -```ts -export const GET: RequestHandler = async ({ url, locals }) => { - const orgId = url.searchParams.get('org_id'); -``` - -**Problem:** The endpoint accepts any `org_id` parameter and fetches calendar data without verifying the requesting user is a member of that org. An unauthenticated user can enumerate org IDs and read all connected Google Calendar events. - -**Fix:** Add session + membership check: -```ts -const { session, user } = await locals.safeGetSession(); -if (!session || !user) return json({ error: 'Unauthorized' }, { status: 401 }); - -const { data: membership } = await locals.supabase - .from('org_members') - .select('id') - .eq('org_id', orgId) - .eq('user_id', user.id) - .single(); - -if (!membership) return json({ error: 'Forbidden' }, { status: 403 }); -``` - ---- - -### S-3 · **HIGH** — Auth callback open redirect vulnerability - -**File:** `src/routes/auth/callback/+server.ts:6` - -```ts -const next = url.searchParams.get('next') ?? url.searchParams.get('redirect') ?? '/'; -``` - -**Problem:** The `next`/`redirect` parameter is used directly in `redirect(303, next)` without validating it's a relative URL. An attacker can craft `?next=https://evil.com` to redirect users after login. - -**Fix:** Validate the redirect target is a relative path: ```ts function safeRedirect(target: string): string { if (target.startsWith('/') && !target.startsWith('//')) return target; return '/'; } -const next = safeRedirect(url.searchParams.get('next') ?? url.searchParams.get('redirect') ?? '/'); ``` --- -### S-4 · **HIGH** — Settings page performs destructive operations without server-side authorization +### S-4 · 🟡 HIGH — Client-side mutations bypass server authorization -**File:** `src/routes/[orgSlug]/settings/+page.svelte:186-463` +**Problem:** Settings page, event pages, and team management perform Supabase mutations directly from the browser. Security relies entirely on RLS policies being correct. The admin dashboard (newly added) correctly uses SvelteKit form actions — this pattern should be adopted everywhere. -**Problem:** All settings mutations (update org, delete org, invite/remove members, manage roles, connect calendar) are done via direct Supabase client calls from the browser. The only server-side check is the page load guard (`owner`/`admin`). If RLS policies are not perfectly configured, any authenticated user could call these mutations directly. +**Affected areas:** +- `[orgSlug]/settings/` — org update, delete, member management +- `[orgSlug]/events/[eventSlug]/` — event update, delete +- `[orgSlug]/events/[eventSlug]/team/` — member add/remove, department CRUD -**Fix:** Move destructive operations to SvelteKit form actions or API routes with explicit server-side authorization checks, rather than relying solely on Supabase RLS. +**Fix:** Migrate destructive operations to SvelteKit form actions with explicit server-side auth checks (like the admin dashboard pattern). --- -### S-5 · **MEDIUM** — `.env.example` uses wrong variable prefix +--- -**File:** `.env.example:1-2` +### S-6 · 🟡 MEDIUM — Document lock RLS race condition -``` -VITE_SUPABASE_URL=your_supabase_url -VITE_SUPABASE_ANON_KEY=your_supabase_anon_key -``` +**File:** `supabase/migrations/016_document_locks.sql` -**Problem:** The example uses `VITE_` prefix but the actual app uses `PUBLIC_` prefix (SvelteKit convention). A developer copying `.env.example` would have a broken setup. +Two competing cleanup paths for expired locks: RLS policy allows any user to delete expired locks, and `acquireLock()` in `document-locks.ts` also deletes them client-side. -**Fix:** Update `.env.example`: -``` -PUBLIC_SUPABASE_URL=your_supabase_url -PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key -GOOGLE_API_KEY=your_google_api_key -``` +**Fix:** Consolidate to one path — either RLS-only or a server-side cron. --- -### S-6 · **MEDIUM** — Document lock RLS allows any user to delete expired locks +### S-7 · 🟡 MEDIUM — `@inlang/paraglide-js` in both deps and devDeps -**File:** `supabase/migrations/016_document_locks.sql:40-41` +**File:** `package.json` -```sql -CREATE POLICY "Anyone can delete expired locks" ON document_locks FOR DELETE - USING (last_heartbeat < now() - interval '60 seconds'); -``` - -**Problem:** Any authenticated user can delete any expired lock. While the intent is correct (cleanup), this could be exploited to race-condition a lock takeover. The `acquireLock` function in `document-locks.ts:75-79` also deletes expired locks client-side, creating two competing cleanup paths. - -**Fix:** Remove the client-side expired lock cleanup from `acquireLock()` and rely solely on the RLS policy, or vice versa. Consider using a server-side cron/function for lock cleanup instead. +`@inlang/paraglide-js` appears in both `dependencies` and `devDependencies`. Should only be in `devDependencies` (it's a build-time tool). --- -## 2. Dead & Unused Code +## 2. Type Safety -### ~~D-1~~ RESOLVED — Dead stores deleted -### ~~D-4~~ RESOLVED — `FileTree` removed -### ~~D-5~~ RESOLVED — `$lib/stores/index.ts` and `auth.svelte.ts` deleted +### T-1 · 🟡 HIGH — Supabase types are stale — 21 `as any` casts remain -### D-2 · **HIGH** — `$lib/utils/api-helpers.ts` is never imported +**Files:** 12 files still use `as any`, concentrated in: +- `src/lib/matrix/sdk-types.ts` (6) — Matrix SDK interop, acceptable +- `src/lib/api/budget.ts`, `contacts.ts`, `schedule.ts`, `sponsors.ts`, `department-dashboard.ts` (5 total) — all use `db()` cast for tables added in migrations 026-031 +- `src/routes/admin/+page.server.ts` (1) — admin uses `db()` for `is_platform_admin` +- `src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.server.ts` (3) +- `src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte` (1) +- `src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte` (1) +- `src/lib/utils/permissions.ts` (1) +- Test files (3) — acceptable in tests -**File:** `src/lib/utils/api-helpers.ts` (96 lines) +**Root cause:** Types haven't been regenerated since migrations 022-031 were added. Tables like `events`, `event_members`, `event_departments`, `department_dashboards`, `budget_categories`, `sponsors`, `schedule_stages`, `department_contacts`, and column `is_platform_admin` on `profiles` are all missing from the generated types. -**Problem:** `unwrap()` and `safeCall()` are well-designed helpers but are never used anywhere. All API modules use manual `if (error) throw error` patterns instead. - -**Fix:** Either delete the file, or migrate API modules to use `unwrap()` (recommended — see finding A-2). +**Fix:** Run `npm run db:types` to regenerate. This single command will eliminate ~15 of the 21 `as any` casts. The remaining 6 (Matrix SDK) are acceptable interop casts. --- -### D-3 · **HIGH** — Entire `layout/` component directory is never imported +### T-2 · 🟡 MEDIUM — Manual type aliases drift from generated types -**Files:** -- `src/lib/components/layout/PageContainer.svelte` -- `src/lib/components/layout/PageHeader.svelte` -- `src/lib/components/layout/ResponsiveGrid.svelte` -- `src/lib/components/layout/SplitPane.svelte` -- `src/lib/components/layout/index.ts` +**File:** `src/lib/supabase/types.ts:1700-1877` -**Problem:** Zero imports of any layout component anywhere in the codebase. - -**Fix:** Delete the entire `src/lib/components/layout/` directory. - ---- - -### D-6 · **MEDIUM** — Unused variables in kanban page - -**File:** `src/routes/[orgSlug]/kanban/+page.svelte:48-49,107` +15 manually-defined interfaces at the bottom of the generated types file (`DepartmentDashboard`, `BudgetCategory`, `Sponsor`, etc.). These will become redundant after type regeneration and could drift from the actual DB schema. +**Fix:** After regenerating types, derive these from the generated `Database` type: ```ts -let newBoardVisibility = $state<"team" | "personal">("team"); -let editBoardVisibility = $state<"team" | "personal">("team"); -let sidebarCollapsed = $state(false); +export type BudgetCategory = Database['public']['Tables']['budget_categories']['Row']; ``` -**Problem:** These three reactive variables are declared but never read or written to anywhere in the template or logic. +--- -**Fix:** Remove all three declarations. +### T-3 · 🟢 GOOD — TypeScript strict mode enabled + +`tsconfig.json` has `"strict": true`, `"checkJs": true`, `"forceConsistentCasingInFileNames": true`. This is best-practice configuration. --- -### D-7 · **MEDIUM** — `$lib/index.ts` is an empty placeholder +## 3. Code Quality & Consistency -**File:** `src/lib/index.ts` +### C-1 · 🟡 HIGH — 41 raw `console.*` calls bypass structured logger -**Problem:** Empty file with only a comment. Serves no purpose. +**13 files** use `console.log/warn/error` directly instead of the project's `createLogger()` system. Concentrated in: +- `src/lib/matrix/client.ts` (10) +- `src/lib/components/matrix/MessageInput.svelte` (5) +- `src/routes/[orgSlug]/chat/+page.svelte` (5) +- `src/routes/api/matrix-provision/+server.ts` (5) +- Various matrix components (6) +- `src/lib/stores/theme.ts` (1) -**Fix:** Delete the file. +**Impact:** Loses structured context, timestamps, and the ring buffer for error reports. The logger is already imported in most non-matrix files. + +**Fix:** Replace all `console.*` with `createLogger()` calls. The Matrix integration was ported from another project and didn't adopt the logger. --- -### D-8 · **MEDIUM** — `$lib/supabase/server.ts` is never imported +### C-2 · 🟡 MEDIUM — Inconsistent error catching: `e: any` vs `e` vs `e: unknown` -**File:** `src/lib/supabase/server.ts` (20 lines) +The codebase uses three different patterns: +- `catch (e: any)` — 12 occurrences (team page, events pages, dept page) +- `catch (e)` — 8 occurrences (tasks page, documents page, chat page) +- `catch (e: unknown)` — 1 occurrence (chat page) -**Problem:** Re-exported as `createServerClient` from `$lib/supabase/index.ts`, but never imported anywhere. All server routes use `locals.supabase` from `hooks.server.ts`. - -**Fix:** Delete `src/lib/supabase/server.ts` and remove the re-export from `src/lib/supabase/index.ts`. +**Best practice:** Use `catch (e: unknown)` and narrow with `e instanceof Error`. The `e: any` pattern loses type safety. --- -### D-9 · **LOW** — Demo test and scaffold test are placeholders +### C-3 · 🟡 MEDIUM — `$lib/stores/ui.ts` uses legacy Svelte stores alongside Svelte 5 runes -**Files:** -- `src/demo.spec.ts` — `it('adds 1 + 2 to equal 3')` -- `src/routes/page.svelte.spec.ts` +**File:** `src/lib/stores/ui.ts` -**Fix:** Delete or replace with real smoke tests. +Uses `writable()` from `svelte/store` for `sidebarOpen`, `membersPanelOpen`, `activeModal`, `modalData`, `isLoading`, `loadingMessage`. The rest of the app uses Svelte 5 `$state()` runes. + +**Fix:** Migrate to runes-based stores using `.svelte.ts` files (like `toast.svelte.ts` already does). --- -## 3. Type Safety & Correctness +### C-4 · 🟡 MEDIUM — 9 a11y warnings from svelte-check -### T-1 · **HIGH** — Supabase types are stale, causing 66 `as any` casts +All in 3 files: +- `SponsorsWidget.svelte` — color picker buttons missing labels (4 warnings) +- `admin/+page.svelte` — form labels not associated with controls (3 warnings) +- `SponsorsWidget.svelte` — buttons without text (2 warnings) -**Files:** 15 files with `as any` casts, concentrated in: -- `src/lib/components/kanban/CardDetailModal.svelte` (9) -- `src/lib/api/document-locks.ts` (6) -- `src/lib/components/documents/FileBrowser.svelte` (6) -- `src/routes/[orgSlug]/documents/[id]/+page.server.ts` (6) -- `src/routes/[orgSlug]/documents/file/[id]/+page.server.ts` (6) -- `src/routes/[orgSlug]/documents/folder/[id]/+page.server.ts` (6) -- `src/routes/[orgSlug]/documents/file/[id]/+page.svelte` (5) -- `src/lib/components/kanban/CardComments.svelte` (4) -- `src/lib/components/kanban/KanbanBoard.svelte` (4) -- `src/routes/api/google-calendar/events/+server.ts` (4) - -**Problem:** The `Database` type in `src/lib/supabase/types.ts` is out of sync with the actual database schema. Missing tables (`document_locks`, `org_google_calendars`, `org_invites`, `org_roles`, `activity_log`), missing columns on existing tables, and incorrect optionality force `as any` everywhere. - -**Fix:** Regenerate types with `supabase gen types typescript --project-id > src/lib/supabase/types.ts`. This single fix will eliminate most of the 66 `as any` casts. +**Fix:** Add `aria-label` attributes to color picker buttons; use `id`/`for` pairs on admin form labels. --- -### T-2 · **MEDIUM** — `user: any` in root page Props +### C-5 · 🟢 GOOD — Consistent API module pattern -**File:** `src/routes/+page.svelte:19` +All 13 API modules in `$lib/api/` follow the same pattern: typed function signatures, Supabase client as first param, structured error logging via `createLogger()`, and thrown errors for callers to handle. This is clean and maintainable. +--- + +### C-6 · 🟢 GOOD — Centralized error handling + +Both server (`hooks.server.ts`) and client (`hooks.client.ts`) have `handleError` functions that generate error IDs, log structured data, and return consistent error shapes. The `+error.svelte` page shows error ID + "Copy Error Report" with recent logs dump. This is production-quality. + +--- + +## 4. Architecture + +### A-1 · 🟡 HIGH — Client-side mutations should use form actions + +The admin dashboard correctly uses SvelteKit form actions for all CRUD. But other pages (settings, events, team management) perform mutations via direct `supabase.from()` calls from the browser. + +**Recommendation:** Adopt the admin dashboard pattern (form actions + `use:enhance`) for all destructive operations. This provides: +- Server-side auth verification +- Progressive enhancement (works without JS) +- Automatic `invalidateAll()` on success + +--- + +### A-2 · 🟡 MEDIUM — Large page components + +Several pages are 500+ lines: +- `[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.svelte` — ~1100 lines (department dashboard) +- `[orgSlug]/events/[eventSlug]/team/+page.svelte` — ~500 lines +- `[orgSlug]/chat/+page.svelte` — ~400 lines +- `admin/+page.svelte` — ~660 lines + +The department dashboard is the largest but is inherently complex (8 module types). The admin page is well-structured with tab separation. + +**Recommendation:** Extract reusable table/list components from admin page for potential reuse in other admin-like views. + +--- + +### A-3 · 🟢 GOOD — Clean module architecture + +The project follows a clear separation: +- `$lib/api/` — Data access layer (13 modules) +- `$lib/components/ui/` — Design system (44 components) +- `$lib/components/modules/` — Feature widgets (8 modules) +- `$lib/stores/` — Global state (4 stores) +- `$lib/utils/` — Utilities (7 files) +- `$lib/matrix/` — Matrix chat integration (isolated) + +--- + +### A-4 · 🟢 GOOD — Server-side auth pattern + +`hooks.server.ts` correctly uses `getUser()` (server-verified) before `getSession()` (client-provided). The `safeGetSession` pattern prevents session spoofing. All page server loads check auth before data fetching. + +--- + +## 5. Performance + +### P-1 · 🟡 MEDIUM — Google Fonts loaded via external CSS import + +**File:** `src/routes/layout.css:1-2` + +```css +@import url('https://fonts.googleapis.com/css2?family=Tilt+Warp&...'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:...'); +``` + +Two render-blocking external CSS imports. Material Symbols font is ~200KB. + +**Fix options:** +1. Self-host fonts (fastest — eliminates external dependency) +2. Add `` to `app.html` +3. Use `font-display: swap` (already set via `&display=swap`) + +--- + +### P-2 · 🟡 MEDIUM — `emojiData.ts` is 40KB + +**File:** `src/lib/utils/emojiData.ts` (40,133 bytes) + +This is a static JSON blob imported at module level. It's included in the client bundle even if the user never opens the emoji picker. + +**Fix:** Lazy-load via dynamic import: ```ts -user: any; // ← should be typed -``` - -**Fix:** Use `import type { User } from '@supabase/supabase-js'` and type as `user: User | null`. - ---- - -### T-3 · **MEDIUM** — Local `OrgWithRole` interface duplicates the API type - -**File:** `src/routes/+page.svelte:9-14` - -```ts -interface OrgWithRole { - id: string; name: string; slug: string; role: string; -} -``` - -**Problem:** This is a simplified duplicate of `$lib/api/organizations.OrgWithRole` (which now extends `Organization`). The server load returns full org fields, but this interface drops `avatar_url`, `created_at`, `updated_at`. - -**Fix:** Import and use the canonical type: `import type { OrgWithRole } from '$lib/api/organizations'`. - ---- - -### T-4 · **MEDIUM** — `org` from parent layout is untyped, causing `(org as any).id` everywhere - -**Files:** All `+page.server.ts` files under `[orgSlug]/documents/`, `[orgSlug]/settings/` - -**Problem:** The parent layout returns `org` as the raw Supabase row, but child loads access it as `(org as any).id`, `(org as any).slug` because TypeScript can't infer the parent return type. - -**Fix:** Create a shared type for the org layout data: -```ts -// $lib/types/layout.ts -export interface OrgLayoutData { - org: Organization; - role: string; - userRole: string; - user: User; - members: any[]; - recentActivity: any[]; -} -``` -Then use it in child loads: `const { org } = await parent() as OrgLayoutData;` - ---- - -### T-5 · **MEDIUM** — `folder: any` in folder page Props - -**File:** `src/routes/[orgSlug]/documents/folder/[id]/+page.svelte:8,17,21` - -```ts -folder: any; -const currentFolderId = $derived((data.folder as any).id as string); -{(data.folder as any).name} -``` - -**Problem:** The folder prop is typed as `any`, requiring double casts. This would be resolved by T-1 (regenerating Supabase types) combined with T-4 (typing parent data). - ---- - -## 4. Code Duplication & Redundancy - -### ~~R-1~~ RESOLVED — `FileBrowser` component extracted, both pages are now thin wrappers (~30 lines each) - -### R-2 · **MEDIUM** — Server loads for documents still repeat the same `as any` pattern - -**Files:** -- `src/routes/[orgSlug]/documents/+page.server.ts` -- `src/routes/[orgSlug]/documents/folder/[id]/+page.server.ts` -- `src/routes/[orgSlug]/documents/file/[id]/+page.server.ts` -- `src/routes/[orgSlug]/documents/[id]/+page.server.ts` - -**Problem:** All four files repeat: get parent data, cast `org as any`, query documents table, handle errors. The `as any` casts are identical across all four. - -**Fix:** Create a shared `getOrgFromParent(parent)` utility that types the parent data once. Resolving T-1 + T-4 would also eliminate this. - ---- - -### R-3 · **MEDIUM** — `$lib/supabase/server.ts` duplicates `hooks.server.ts` - -**Problem:** `server.ts` creates a server Supabase client from cookies, but `hooks.server.ts` already does this and puts it on `locals.supabase`. The `server.ts` export is never imported. - -**Fix:** Delete `src/lib/supabase/server.ts` and remove the `createServerClient` re-export from `src/lib/supabase/index.ts`. - ---- - -### R-4 · **LOW** — `role` and `userRole` are the same value returned from org layout - -**File:** `src/routes/[orgSlug]/+layout.server.ts:74-75` - -```ts -role: membership.role, -userRole: membership.role, // kept for backwards compat — same as role -``` - -**Problem:** Both contain `membership.role`. The layout uses `data.role`, child pages use `data.userRole`. The comment acknowledges the duplication. - -**Fix:** Migrate all consumers to `userRole` and remove `role`, or vice versa. - ---- - -## 5. Error Handling & Resilience - -### E-1 · **HIGH** — Settings page uses `alert()` and swallows errors - -**File:** `src/routes/[orgSlug]/settings/+page.svelte` - -- Line 234: `alert("Failed to send invite: " + error.message)` -- Line 447: `alert("Owners cannot leave...")` -- Lines 240, 274, 357, 426, 440: No error handling on `await supabase.from(...).delete()` — errors are silently ignored. - -**Fix:** Use the existing `toasts` store for user feedback. Add error handling to all mutations: -```ts -const { error } = await supabase.from('org_members').delete().eq('id', memberId); -if (error) { toasts.error('Failed to remove member'); return; } +const emojiData = await import('$lib/utils/emojiData'); ``` --- -### E-2 · **HIGH** — `console.error` used instead of structured logger in 6 files +### P-3 · 🟢 GOOD — Optimistic updates on kanban -**Files (11 instances):** -- `src/routes/api/google-calendar/events/+server.ts` — 4 instances (`console.error`, `console.log`) -- `src/routes/[orgSlug]/calendar/+page.svelte` — 3 instances -- `src/routes/invite/[token]/+page.svelte` — 2 instances -- `src/lib/api/google-calendar.ts` — 1 instance -- `src/routes/[orgSlug]/kanban/+page.svelte` — 1 instance - -**Problem:** The project has a well-designed `createLogger()` system but these modules bypass it with raw `console.*` calls, losing structured context, timestamps, and the ring buffer for error reports. - -**Fix:** Replace all `console.error/log/warn` with `createLogger()` calls. +Card moves use synchronous state updates + fire-and-forget DB persist. Realtime handlers use incremental diffs instead of full reloads. `optimisticMoveIds` Set suppresses realtime echoes. This is well-implemented. --- -### E-3 · **MEDIUM** — Calendar page `handleDateClick` is a no-op +### P-4 · 🟢 GOOD — Vite config optimized for Windows -**File:** `src/routes/[orgSlug]/calendar/+page.svelte:41-43` - -```ts -function handleDateClick(_date: Date) { - // Event creation disabled -} -``` - -**Fix:** Either implement event creation or remove the click handler from the Calendar component to avoid misleading UX. +File watcher ignores `node_modules`, `test-results`, `.svelte-kit`, and paraglide cache. Matrix crypto WASM excluded from SSR bundling. This prevents common Windows dev server performance issues. --- -### E-4 · **MEDIUM** — `data` captured by value causes stale state in multiple pages +## 6. Testing -**Files:** -- `src/routes/[orgSlug]/calendar/+page.svelte:26` — `let events = $state(data.events)` -- `src/routes/[orgSlug]/documents/+page.svelte:15` — `let documents = $state(data.documents)` -- `src/routes/[orgSlug]/documents/folder/[id]/+page.svelte:16` — `let documents = $state(data.documents)` -- `src/routes/[orgSlug]/kanban/+page.svelte:40` — `let boards = $state(data.boards)` -- `src/routes/+page.svelte:27` — `let organizations = $state(data.organizations)` +### TS-1 · 🟡 MEDIUM — No Svelte component tests -**Problem:** Svelte warns: "This reference only captures the initial value of `data`." If `data` changes (e.g., via `invalidateAll()`), the local state won't update. +112 unit tests exist but all test pure TypeScript functions (API modules, utils, markdown parsing). No Svelte component rendering tests despite `vitest-browser-svelte` being installed and configured. -**Fix:** Add `$effect` blocks to sync: -```ts -let events = $state(data.events); -$effect(() => { events = data.events; }); -``` +**Priority components to test:** +- `Button`, `Modal`, `Input`, `TabBar` — core UI +- `Avatar` — name-to-color hashing logic +- `StatusBadge` — status-to-color mapping --- -### E-5 · **MEDIUM** — `releaseLock` called in `onDestroy` may not complete +### TS-2 · 🟡 MEDIUM — E2E tests don't cover new features -**File:** `src/routes/[orgSlug]/documents/file/[id]/+page.svelte:222-225` - -```ts -onDestroy(() => { - // ... - releaseLock(supabase, data.document.id, data.user.id); -}); -``` - -**Problem:** `releaseLock` is async but not awaited. In `onDestroy`, the component is being torn down and the async call may not complete before the page unloads (especially on navigation). The lock would then rely on heartbeat expiry (60s) to be cleaned up. - -**Fix:** Use `beforeunload` event for page unloads and `navigator.sendBeacon` for reliable cleanup: -```ts -window.addEventListener('beforeunload', () => { - navigator.sendBeacon(`/api/release-lock`, JSON.stringify({ documentId, userId })); -}); -``` +Playwright tests cover: auth, org CRUD, documents, kanban, calendar. But missing: +- Events CRUD flow +- Department dashboard +- Admin dashboard +- Chat integration +- Team management --- -## 6. Architecture & Structure +### TS-3 · 🟢 GOOD — Test infrastructure is solid -### A-1 · **HIGH** — Settings page is a 1205-line god component - -**File:** `src/routes/[orgSlug]/settings/+page.svelte` (1205 lines) - -**Problem:** This single file handles 5 tabs (General, Members, Roles, Integrations, Appearance), each with their own state, modals, and API calls. It contains ~15 async functions, ~25 state variables, and 6 modals. - -**Fix:** Extract each tab into its own component: -- `SettingsGeneral.svelte` -- `SettingsMembers.svelte` -- `SettingsRoles.svelte` -- `SettingsIntegrations.svelte` -- `SettingsAppearance.svelte` +- Vitest with browser + server projects +- `requireAssertions: true` prevents empty tests +- Playwright with auth setup, cleanup, and CI config +- 112 passing tests with 0 failures --- -### A-2 · **MEDIUM** — Inconsistent data fetching patterns +## 7. Dependencies -**Problem:** The codebase uses three different patterns for Supabase calls: -1. **API modules** (`$lib/api/*.ts`) — used by kanban page, org overview, document locks -2. **Direct `supabase.from()` in components** — used by settings, FileBrowser, card detail modal, comments -3. **Server loads** — used by all `+page.server.ts` files +### DEP-1 · 🟡 MEDIUM — `@inlang/paraglide-js` duplicated in deps -Pattern 2 is problematic because it bypasses the API layer's logging and error handling. The new `FileBrowser` component (872 lines) does all mutations via direct Supabase calls despite `$lib/api/documents.ts` having `createDocument`, `updateDocument`, `moveDocument`, `deleteDocument` functions. - -**Fix:** Migrate `FileBrowser` to use the documents API module. Add missing `kanban` type support to `createDocument`. +Listed in both `dependencies` and `devDependencies`. Should only be in `devDependencies`. --- -### A-3 · **MEDIUM** — `createDocument` API doesn't support `kanban` type +### DEP-2 · 🟡 LOW — `@types/twemoji` may be unnecessary -**File:** `src/lib/api/documents.ts:30` - -```ts -type: 'folder' | 'document', // missing 'kanban' -``` - -**Problem:** The function signature only accepts `'folder' | 'document'` but the database and UI support `'kanban'` as a document type. `FileBrowser.handleCreate()` calls Supabase directly to create kanban documents. - -**Fix:** Update the type to `'folder' | 'document' | 'kanban'` and add kanban board creation logic. +`twemoji` v14 may ship its own types. Check if removing `@types/twemoji` causes any errors. --- -## 7. Performance +### DEP-3 · 🟢 GOOD — All dependencies actively used -### P-1 · **HIGH** — Folder page loads ALL org documents on every visit - -**File:** `src/routes/[orgSlug]/documents/folder/[id]/+page.server.ts` - -**Problem:** Every folder page load fetches every document in the entire org (including content blobs via `select('*')`). For orgs with many documents, this is wasteful — content is fetched but only names/types are displayed in the file browser. - -**Fix:** -1. Select only needed columns: `.select('id, name, type, parent_id, created_at, updated_at')` -2. Consider fetching only the current folder's children + ancestors for breadcrumbs. +Every package in `dependencies` has corresponding imports: +- `@supabase/ssr` + `@supabase/supabase-js` — core data layer +- `@tanstack/svelte-virtual` — virtualized lists in chat +- `@tiptap/*` — rich text editor +- `google-auth-library` — calendar integration +- `highlight.js` + `marked` — markdown rendering +- `matrix-js-sdk` — chat +- `twemoji` — emoji rendering --- -### P-2 · **HIGH** — `fetchBoardWithColumns` makes 3 sequential queries +## 8. DevOps & Infrastructure -**File:** `src/lib/api/kanban.ts:33-86` +### DO-1 · 🟢 GOOD — Docker setup -**Problem:** Fetches board, then columns, then cards in 3 separate queries. The columns and cards queries could run in parallel. - -**Fix:** After fetching the board, run columns and cards queries in parallel with `Promise.all`, or use a single query with joins. +Multi-stage Dockerfile with builder + production stages. Health check endpoint at `/health`. `docker-compose.yml` with both production and dev services. Environment variables properly externalized. --- -### P-3 · **MEDIUM** — `moveCard` in API makes N individual UPDATE queries +### DO-2 · 🟢 GOOD — Database migrations -**File:** `src/lib/api/kanban.ts:260-271` - -**Problem:** Moving a card fires one UPDATE query per card in the target column. A column with 20 cards = 20 queries. - -**Fix:** Use a Supabase RPC function for batch position updates, or only update cards whose position actually changed. +31 sequential migrations with clear naming. RLS policies on all tables. Platform admin bypass policies (migration 031). Auto-create triggers for department dashboards. --- -### P-4 · **MEDIUM** — Realtime subscription reloads entire board on any change +### DO-3 · 🟡 MEDIUM — No database seed file -**Files:** -- `src/routes/[orgSlug]/kanban/+page.svelte:64-74` -- `src/routes/[orgSlug]/documents/file/[id]/+page.svelte:199-215` +No `supabase/seed.sql` for development setup. New developers must manually create test data. -**Problem:** Any card or column change (even by the current user) triggers a full board reload (3 queries). Combined with optimistic updates, this can cause UI flicker and wasted bandwidth. - -**Fix:** Use the realtime payload to apply incremental updates instead of full reloads. +**Fix:** Create a seed file with sample org, users, events, and departments. --- -## 8. Dependency Health +## 9. Accessibility -### DEP-1 · **MEDIUM** — `lucide-svelte` is installed but never imported +### A11Y-1 · 🟡 MEDIUM — 9 svelte-check a11y warnings -**File:** `package.json:40` - -```json -"lucide-svelte": "^0.563.0" -``` - -**Problem:** Zero imports of `lucide-svelte` anywhere in the codebase. The app uses Google Material Symbols for all icons. - -**Fix:** `npm uninstall lucide-svelte` — saves bundle size. +- Color picker buttons in `SponsorsWidget.svelte` lack labels +- Admin page form labels not associated with controls +- All are fixable with `aria-label` or `id`/`for` attributes --- -### DEP-2 · **LOW** — `@tailwindcss/forms` may be unused +### A11Y-2 · 🟢 GOOD — Modal accessibility -**Problem:** The plugin is listed as a devDependency and imported in `layout.css`, but all form elements are custom-styled via the `Input`, `Select`, `Textarea` components. The forms plugin may be adding base styles that are immediately overridden. - -**Fix:** Test removing `@tailwindcss/forms` to see if anything breaks. If not, remove it. +`Modal.svelte` has `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, keyboard escape handling, and backdrop click. This follows WAI-ARIA dialog pattern. --- -## 9. Maintainability & Readability +### A11Y-3 · 🟢 GOOD — Error page -### M-1 · **MEDIUM** — Magic numbers for invite expiry - -**File:** `src/routes/[orgSlug]/settings/+page.svelte:220-222` - -```ts -expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), -``` - -**Fix:** Extract to a constant: `const INVITE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days` - ---- - -### M-2 · **MEDIUM** — Inconsistent error feedback patterns - -**Problem:** The codebase uses 4 different patterns for user-facing errors: -1. `alert()` — settings page -2. `toasts.error()` — documents, file viewer -3. `console.error()` only (silent to user) — kanban, calendar, invite pages -4. Inline `error` state variable — login page, invite page - -**Fix:** Standardize on `toasts` for all user-facing error feedback. - ---- - -### M-3 · **LOW** — Inline SVG icons in multiple places - -**Files:** -- `src/routes/+page.svelte:88-98,105-116` — inline SVG for plus icon, people icon -- `src/routes/[orgSlug]/settings/+page.svelte` — inline SVGs for invite, Google, Discord, Slack icons -- `src/routes/invite/[token]/+page.svelte:103-136` — inline SVGs for error/invite icons - -**Fix:** Use `<Icon name="..." />` for Material icons. For brand logos, create a `BrandIcon` component or use static SVG imports. - ---- - -## 10. Future-Proofing - -### F-1 · **MEDIUM** — No permission enforcement beyond role checks - -**Problem:** The settings page defines a granular permission system (`documents.view`, `kanban.create`, `members.manage`, etc.) and roles can have custom permissions. But no code anywhere actually checks these permissions — only `role === 'owner' || role === 'admin'` checks exist. - -**Fix:** Create a `hasPermission(userRole, roles, permission)` utility and use it to gate actions throughout the app. - ---- - -### F-2 · **MEDIUM** — Kanban board subscription listens to ALL card changes - -**File:** `src/lib/api/kanban.ts:291` - -```ts -.on('postgres_changes', { event: '*', schema: 'public', table: 'kanban_cards' }, onCardChange) -``` - -**Problem:** No filter on `column_id` or board. This subscription fires for every card change across all boards in the entire database. - -**Fix:** Add a filter or use a Supabase function to scope the subscription to the current board's column IDs. - ---- - -### F-3 · **LOW** — `@tailwindcss/typography` prose styles may conflict with editor - -**Problem:** The TipTap editor content is wrapped in `.prose` which applies Tailwind Typography styles. These may conflict with the editor's own styling. - -**Fix:** Audit the `.prose` overrides in `layout.css` and consider scoping them more tightly. +`+error.svelte` shows status code, message, error ID, context, and a "Copy Error Report" button with recent logs. Excellent for debugging. --- --- -## Summary +## Summary Scorecard (v5) -### Issues by Severity +| Area | Score | Key Notes | +|------|-------|-----------| +| **Security** | 3/5 | S-1 (credential rotation) still critical. S-4 (server-side auth) partially addressed by admin form actions. RLS bypass for platform admins added. | +| **Type Safety** | 4/5 | `strict: true`. 21 `as any` casts remain, 15 fixable by `npm run db:types`. Matrix SDK casts are acceptable. | +| **Code Quality** | 4/5 | Consistent API pattern. 41 raw `console.*` calls in Matrix code. Inconsistent error catch typing. | +| **Architecture** | 4/5 | Clean module separation. Admin dashboard uses form actions (good pattern). Other pages still use client-side mutations. | +| **Performance** | 4/5 | Optimistic updates, incremental realtime, Windows-optimized watcher. External font loading and 40KB emoji data could improve. | +| **Testing** | 3.5/5 | 112 unit tests, Playwright E2E, CI pipeline. Missing: component tests, new feature E2E coverage. | +| **Dependencies** | 4.5/5 | All deps used. One duplicate (`paraglide-js`). | +| **DevOps** | 4/5 | Docker, health checks, 31 migrations. Missing: seed file. | +| **Accessibility** | 4/5 | Good modal/error patterns. 9 minor a11y warnings. | +| **Error Handling** | 4/5 | Structured logger, error IDs, ring buffer, toast system. Matrix code bypasses logger. | -| Severity | Count | -|----------|-------| -| **Critical** | 2 | -| **High** | 7 | -| **Medium** | 17 | -| **Low** | 4 | -| **Total** | **30** | - -### Resolved Since v1 - -| ID | Issue | Resolution | -|----|-------|------------| -| D-1 | 3 dead stores (660 lines) | Deleted (auth, organizations, documents, kanban, theme) | -| D-4 | Unused `FileTree` component | Removed from index and codebase | -| D-5 | Dead `$lib/stores/index.ts` + `auth.svelte.ts` | Deleted | -| R-1 | Documents page duplication (~1900 lines) | Extracted `FileBrowser` component; both pages now ~30 lines | -| R-4 | Kanban store vs inline optimistic updates | Store deleted; inline implementation kept | -| R-6 | `Promise.resolve(null)` placeholder | Layout load refactored | -| — | Calendar `$derived` vs `$derived.by` bug | Fixed (`weeks`, `headerTitle`) | -| — | `buildDocumentTree`/`DocumentWithChildren` dead code | Removed | -| — | Editor CSS typo (`py-1.5bg-background`) | Fixed to `py-1.5 bg-background` | -| — | Invite page wrong routes (`/signup`, `/logout`) | Fixed to `/login?tab=signup`, `/auth/logout` | -| — | KanbanBoard "Add column" mislabel | Fixed to "Add card" | - -### Resolved Since v2 - -| ID | Issue | Resolution | -|----|-------|------------| -| S-2 | Google Calendar API no auth | Added session + org membership check; returns 401/403 | -| S-3 | Auth callback open redirect | Added `safeRedirect()` validator — only allows relative paths | -| S-5 | `.env.example` wrong prefix | Fixed `VITE_` → `PUBLIC_` | -| D-2 | Unused `api-helpers.ts` (96 lines) | Deleted | -| D-3 | Unused `layout/` directory (5 files) | Deleted entire directory | -| D-6 | Unused kanban vars (`newBoardVisibility`, `editBoardVisibility`, `sidebarCollapsed`) | Removed | -| D-7 | Empty `$lib/index.ts` | Deleted | -| D-8 | Unused `$lib/supabase/server.ts` | Deleted; removed re-export from index | -| D-9 | Placeholder tests (`demo.spec.ts`, `page.svelte.spec.ts`) | Deleted | -| DEP-1 | Unused `lucide-svelte` dependency | Uninstalled | -| E-1 | Settings page `alert()` calls | Replaced with `toasts` store | -| E-2 | `console.*` in Google Calendar API route | Replaced with `createLogger()` | -| E-4 | Stale `$state(data.*)` in 5 pages | Added `$effect` sync blocks | -| A-1 | Settings page god component (partial) | Extracted `SettingsGeneral` component; Figma-style pill tab nav | -| R-3 | `server.ts` duplicates `hooks.server.ts` | Deleted (same as D-8) | - -### Top 3 Highest-Impact Remaining Improvements - -1. **Rotate credentials & secure `.env`** (S-1) — Immediate security risk. Takes 15 minutes but prevents catastrophic data breach. **Must be done manually.** - -2. **Regenerate Supabase types** (T-1) — Single command that eliminates 66 `as any` casts, all `never` type errors, and makes the entire codebase type-safe. Also resolves T-4, T-5, and most of R-2. - -3. **Continue splitting settings page** (A-1) — Members, Roles, and Integrations tabs still inline. Extract each into its own component. - -### Resolved Since v3 - -| ID | Issue | Resolution | -|----|-------|------------| -| — | Icon buttons not round | All inline icon buttons (`rounded-lg`) changed to `rounded-full` across KanbanCard, KanbanBoard, DocumentViewer, Calendar, Modal, ContextMenu | -| — | Add column/card buttons missing plus icon | Replaced inline buttons with `Button` component using `icon="add"` prop | -| — | Kanban columns not reorderable | Added column drag-and-drop with grip handle, drop indicators, and DB persistence | -| — | Inconsistent cursor styles | Added global CSS rules: `cursor-pointer` on all `button`/`a`/`[role="button"]`, `cursor-grab` on `[draggable="true"]` | -| — | Blurred spinner loading overlay | Replaced `backdrop-blur-sm` spinner with context-aware `PageSkeleton` component (kanban/files/calendar/settings/default variants) | -| — | Language switcher missing | Added locale picker (English/Eesti) to account settings using Paraglide `setLocale()` | -| — | File browser view mode not persisted | Confirmed already working via `localStorage` (`root:viewMode` key) | - -### Resolved Since v4 - -| ID | Issue | Resolution | -|----|-------|------------| -| T-1 | 2 remaining `as any` casts | Replaced with properly typed casts in invite page and CardDetailModal | -| T-2→T-5 | Untyped parent layout data | Created shared `OrgLayoutData` type in `$lib/types/layout.ts`; applied across all 8 page servers | -| R-4 | Duplicate `role`/`userRole` | Removed `role` from layout server return; migrated all consumers to `userRole` | -| A-1 | Settings page god component (1200+ lines) | Extracted `SettingsMembers`, `SettingsRoles`, `SettingsIntegrations` into `$lib/components/settings/`; page reduced to ~470 lines | -| A-2 | FileBrowser direct Supabase calls | Migrated all CRUD operations to use `$lib/api/documents.ts` (`moveDocument`, `updateDocument`, `deleteDocument`, `createDocument`, `copyDocument`) | -| A-3 | `createDocument` missing kanban type | Added `'kanban'` to type union with optional `id` and `content` params | -| E-3 | Calendar date click no-op | Already implemented — clicking a day opens create event modal pre-filled with date | -| P-1 | Folder listings fetch `select('*')` | Changed to select only metadata columns, excluding heavy `content` JSON | -| P-2 | Kanban queries sequential | Board+columns now fetched in parallel; tags+checklists+assignees fetched in parallel | -| P-3 | `moveCard` fires N updates | Now skips cards whose position didn't change — typically 2-3 updates instead of N | -| P-4 | Realtime full board reload | Upgraded `subscribeToBoard` to pass granular payloads; kanban page applies INSERT/UPDATE/DELETE diffs incrementally | -| T6 | No unit tests | Added 43 Vitest unit tests: `logger.test.ts` (10), `google-calendar.test.ts` (11), `calendar.test.ts` (12), `documents.test.ts` (10) | -| T6 | Incomplete E2E coverage | Added Playwright tests for Tags tab, calendar CRUD (create/view/delete), kanban card CRUD (create/detail modal) | -| T6 | No CI pipeline | Created `.github/workflows/ci.yml`: lint → check → unit tests → build | -| T6 | Test cleanup incomplete | Updated `cleanup.ts` to handle test tags, calendar events, and new board prefixes | +### Overall: 4.0 / 5.0 --- -## Area Scores (v4) +## Priority Action Items -Scores reflect the current state of the codebase after all v1–v4 fixes. +### Tier 1 — Critical (do now) -| Area | Score | Notes | -|------|-------|-------| -| **Security** | ⭐⭐⭐ 3/5 | S-2, S-3, S-5 fixed. **S-1 (credential rotation) and S-4 (server-side auth for mutations) remain critical/high.** S-6 (lock cleanup race) still open. | -| **Type Safety** | ⭐⭐⭐⭐ 4/5 | `OrgLayoutData` shared type eliminates parent casts. 2 targeted `as any` casts fixed. Remaining `as any` casts are in Supabase join results that need full type regeneration (T-1). | -| **Dead Code** | ⭐⭐⭐⭐⭐ 5/5 | All dead stores, unused components, placeholder tests, empty files, and unused dependencies removed in v2. No known dead code remains. | -| **Architecture** | ⭐⭐⭐⭐ 4/5 | Settings page split into 4 components. FileBrowser migrated to API modules. `createDocument` supports all types. Remaining: some components still have inline Supabase calls (CardDetailModal, CardComments). | -| **Performance** | ⭐⭐⭐⭐ 4/5 | Folder listings exclude content. Kanban queries parallelized. Card moves batched smartly. Realtime is incremental. Remaining: full org document fetch for breadcrumbs could be optimized further. | -| **Error Handling** | ⭐⭐⭐⭐ 4/5 | `alert()` replaced with toasts. Structured logger adopted in API routes. `$effect` sync blocks added. Remaining: `console.error` in 3-4 files (calendar page, invite page), lock release in `onDestroy`. | -| **Testing** | ⭐⭐⭐⭐ 4/5 | 43 unit tests (logger, calendar, google-calendar, documents API). 35+ Playwright E2E tests covering all major flows. CI pipeline on GitHub Actions. Remaining: visual regression tests, Svelte component tests. | -| **Code Quality** | ⭐⭐⭐⭐ 4/5 | Consistent API module pattern. Shared types. i18n complete. Duplication eliminated. Remaining: `role`/`userRole` fully migrated but some inline SVGs and magic numbers persist. | -| **Dependencies** | ⭐⭐⭐⭐⭐ 5/5 | `lucide-svelte` removed. All deps actively used. No known unused packages. | -| **Future-Proofing** | ⭐⭐⭐ 3/5 | Permission system defined but not enforced (F-1). Kanban realtime subscription unscoped (F-2). No search, notifications, or keyboard shortcuts yet. | +1. **S-1: Purge `.env` from git history and rotate all secrets** + - `git rm --cached .env && git commit` + - Use `git filter-repo` to purge from history + - Rotate: Supabase keys, Google API key, Google service account, Matrix admin token -### Overall Score: ⭐⭐⭐⭐ 4.0 / 5 +### Tier 2 — High (this week) -**Breakdown:** 41 out of 50 possible stars across 10 areas. +2. **T-1: Regenerate Supabase types** — `npm run db:types` eliminates 15 `as any` casts +3. **C-1: Replace `console.*` with `createLogger()`** in Matrix integration files +4. **S-3: Fix auth callback open redirect** — add `safeRedirect()` validator +5. **A11Y-1: Fix 9 a11y warnings** — add `aria-label` and `id`/`for` attributes -### Remaining High-Priority Items +### Tier 3 — Medium (this month) -1. **S-1: Rotate credentials & purge `.env` from git history** — Critical security risk. Must be done manually. -2. **S-4: Server-side auth for settings mutations** — Move destructive operations to SvelteKit form actions with explicit authorization. -3. **T-1: Regenerate Supabase types** — `supabase gen types typescript` to eliminate remaining `as any` casts from join results. -4. **F-1: Permission enforcement** — Create `hasPermission()` utility; the permission system is defined but never checked. +6. **S-4/A-1: Migrate mutations to form actions** — start with settings page +7. **C-3: Migrate `$lib/stores/ui.ts` to runes** — align with Svelte 5 patterns +8. **P-1: Self-host fonts** or add preconnect hints +9. **P-2: Lazy-load emoji data** +10. **TS-1: Add Svelte component tests** for core UI components +11. **DO-3: Create database seed file** +12. **DEP-1: Remove duplicate `paraglide-js`** from `dependencies` -### Remaining Medium-Priority Items +### Tier 4 — Low (backlog) -5. **S-6: Lock cleanup race condition** — Consolidate to server-side cron only. -6. **E-2: Replace remaining `console.*` calls** — 3-4 files still use raw console instead of structured logger. -7. **E-5: Lock release in `onDestroy`** — Use `navigator.sendBeacon` for reliable cleanup. -8. **F-2: Scoped realtime subscriptions** — Filter kanban card changes to current board's columns. -9. **M-1/M-3: Magic numbers and inline SVGs** — Extract constants, use Icon component consistently. +13. **C-2: Standardize error catching** to `catch (e: unknown)` +14. **TS-2: Expand E2E tests** to cover events, departments, admin, chat +15. **T-2: Derive manual type aliases** from generated types after regeneration +16. **S-6: Consolidate lock cleanup** to single path +17. **DEP-2: Check if `@types/twemoji` is needed** -### Feature Backlog (Tier 5) +### Feature Backlog -10. Notifications system (mentions, assignments, due dates) -11. Global search across documents, kanban cards, calendar events -12. Keyboard shortcuts for common actions -13. Mobile responsive layout (sidebar drawer, touch-friendly kanban) -14. Dark/light theme toggle -15. Export/import (CSV/JSON/Markdown) -16. Undo/redo with toast-based undo for destructive actions -17. Onboarding flow for new users -18. Visual regression tests for key pages +18. Permission enforcement (`hasPermission()` utility) +19. Global search across all entities +20. Keyboard shortcuts +21. Notifications system +22. Mobile responsive layout +23. Undo/redo with toast-based undo +24. Onboarding flow for new users diff --git a/package.json b/package.json index d66fd41..772c634 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "vitest-browser-svelte": "^2.0.2" }, "dependencies": { - "@inlang/paraglide-js": "^2.10.0", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.94.0", "@tanstack/svelte-virtual": "^3.13.18", diff --git a/src/app.html b/src/app.html index 67f126a..9947c28 100644 --- a/src/app.html +++ b/src/app.html @@ -1,13 +1,14 @@ <!doctype html> <html lang="%paraglide.lang%"> + <head> <meta charset="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1" - /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> @@ -22,4 +23,5 @@ <body data-sveltekit-preload-data="tap"> <div style="display: contents">%sveltekit.body%</div> </body> -</html> + +</html> \ No newline at end of file diff --git a/src/lib/components/matrix/CreateRoomModal.svelte b/src/lib/components/matrix/CreateRoomModal.svelte index 1401977..304f027 100644 --- a/src/lib/components/matrix/CreateRoomModal.svelte +++ b/src/lib/components/matrix/CreateRoomModal.svelte @@ -2,6 +2,9 @@ import { Button, Input } from "$lib/components/ui"; import { createRoom } from "$lib/matrix"; import { toasts } from "$lib/stores/ui"; + import { createLogger, getErrorMessage } from "$lib/utils/logger"; + + const log = createLogger('matrix:room'); import { syncRoomsFromEvent, selectRoom } from "$lib/stores/matrix"; interface Props { @@ -35,9 +38,9 @@ roomName = ""; isDirect = false; onClose(); - } catch (e: any) { - console.error("Failed to create room:", e); - toasts.error(e.message || "Failed to create room"); + } catch (e: unknown) { + log.error('Failed to create room', { error: e }); + toasts.error(getErrorMessage(e, 'Failed to create room')); } finally { isCreating = false; } diff --git a/src/lib/components/matrix/CreateSpaceModal.svelte b/src/lib/components/matrix/CreateSpaceModal.svelte index feccaf6..80231da 100644 --- a/src/lib/components/matrix/CreateSpaceModal.svelte +++ b/src/lib/components/matrix/CreateSpaceModal.svelte @@ -2,6 +2,9 @@ import { Button, Input } from "$lib/components/ui"; import { createSpace, getSpaces } from "$lib/matrix"; import { toasts } from "$lib/stores/ui"; + import { createLogger, getErrorMessage } from "$lib/utils/logger"; + + const log = createLogger('matrix:space'); import { syncRoomsFromEvent } from "$lib/stores/matrix"; interface Props { @@ -45,9 +48,9 @@ spaceTopic = ""; isPublic = false; onClose(); - } catch (e: any) { - console.error("Failed to create space:", e); - toasts.error(e.message || "Failed to create space"); + } catch (e: unknown) { + log.error('Failed to create space', { error: e }); + toasts.error(getErrorMessage(e, 'Failed to create space')); } finally { isCreating = false; } diff --git a/src/lib/components/matrix/MessageInput.svelte b/src/lib/components/matrix/MessageInput.svelte index 81afe48..70e2daf 100644 --- a/src/lib/components/matrix/MessageInput.svelte +++ b/src/lib/components/matrix/MessageInput.svelte @@ -20,6 +20,9 @@ import EmojiPicker from "$lib/components/ui/EmojiPicker.svelte"; import { convertEmojiShortcodes } from "$lib/utils/emojiData"; import { getTwemojiUrl } from "$lib/utils/twemoji"; + import { createLogger, getErrorMessage } from "$lib/utils/logger"; + + const log = createLogger('matrix:input'); // Emoji detection regex const emojiRegex = @@ -134,11 +137,11 @@ } // Send typing indicator - setTyping(roomId, true).catch(console.error); + setTyping(roomId, true).catch((e) => log.error('Failed to send typing', { error: e })); // Stop typing after 3 seconds of no input typingTimeout = setTimeout(() => { - setTyping(roomId, false).catch(console.error); + setTyping(roomId, false).catch((e) => log.error('Failed to stop typing', { error: e })); }, 3000); } @@ -416,7 +419,7 @@ clearTimeout(typingTimeout); typingTimeout = null; } - setTyping(roomId, false).catch(console.error); + setTyping(roomId, false).catch((e) => log.error('Failed to stop typing', { error: e })); // Create a temporary event ID for the pending message const tempEventId = `pending-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; @@ -462,11 +465,11 @@ // If no event ID returned, just mark as not pending confirmPendingMessage(roomId, tempEventId, tempEventId); } - } catch (e: any) { - console.error("Failed to send message:", e); + } catch (e: unknown) { + log.error('Failed to send message', { error: e }); // Remove the pending message on failure removePendingMessage(roomId, tempEventId); - toasts.error(e.message || "Failed to send message"); + toasts.error(getErrorMessage(e, 'Failed to send message')); } finally { isSending = false; // Refocus after DOM settles from optimistic update @@ -497,9 +500,9 @@ const contentUri = await uploadFile(file); await sendFileMessage(roomId, file, contentUri); toasts.success("File sent!"); - } catch (e: any) { - console.error("Failed to upload file:", e); - toasts.error(e.message || "Failed to upload file"); + } catch (e: unknown) { + log.error('Failed to upload file', { error: e }); + toasts.error(getErrorMessage(e, 'Failed to upload file')); } finally { isUploading = false; } diff --git a/src/lib/components/matrix/RoomSettingsModal.svelte b/src/lib/components/matrix/RoomSettingsModal.svelte index 39c91d0..20b599b 100644 --- a/src/lib/components/matrix/RoomSettingsModal.svelte +++ b/src/lib/components/matrix/RoomSettingsModal.svelte @@ -2,6 +2,9 @@ import { Avatar } from "$lib/components/ui"; import { setRoomName, setRoomTopic, setRoomAvatar } from "$lib/matrix"; import { toasts } from "$lib/stores/ui"; + import { createLogger } from "$lib/utils/logger"; + + const log = createLogger('matrix:settings'); import { syncRoomsFromEvent } from "$lib/stores/matrix"; import type { RoomSummary } from "$lib/matrix/types"; @@ -55,7 +58,7 @@ toasts.success("Room settings updated"); onClose(); } catch (e) { - console.error("Failed to update room settings:", e); + log.error('Failed to update room settings', { error: e }); toasts.error("Failed to update room settings"); } finally { isSaving = false; diff --git a/src/lib/components/matrix/StartDMModal.svelte b/src/lib/components/matrix/StartDMModal.svelte index 32568dc..5b86687 100644 --- a/src/lib/components/matrix/StartDMModal.svelte +++ b/src/lib/components/matrix/StartDMModal.svelte @@ -2,6 +2,9 @@ import { Avatar } from '$lib/components/ui'; import { searchUsers, createDirectMessage } from '$lib/matrix'; import { toasts } from '$lib/stores/ui'; + import { createLogger, getErrorMessage } from '$lib/utils/logger'; + + const log = createLogger('matrix:dm'); interface Props { onClose: () => void; @@ -29,7 +32,7 @@ try { searchResults = await searchUsers(searchQuery); } catch (e) { - console.error('Search failed:', e); + log.error('Search failed', { error: e }); } finally { isSearching = false; } @@ -43,9 +46,9 @@ toasts.success('Direct message started!'); onDMCreated(roomId); onClose(); - } catch (e: any) { - console.error('Failed to create DM:', e); - toasts.error(e.message || 'Failed to start direct message'); + } catch (e: unknown) { + log.error('Failed to create DM', { error: e }); + toasts.error(getErrorMessage(e, 'Failed to start direct message')); } finally { isCreating = false; } diff --git a/src/lib/components/matrix/SyncRecoveryBanner.svelte b/src/lib/components/matrix/SyncRecoveryBanner.svelte index eb83b06..de8eb5e 100644 --- a/src/lib/components/matrix/SyncRecoveryBanner.svelte +++ b/src/lib/components/matrix/SyncRecoveryBanner.svelte @@ -1,6 +1,9 @@ <script lang="ts"> import { syncState, syncError, clearState } from "$lib/stores/matrix"; import { clearAllCache } from "$lib/cache"; + import { createLogger } from "$lib/utils/logger"; + + const log = createLogger('matrix:sync'); interface Props { onHardRefresh?: () => void; @@ -43,7 +46,7 @@ // Reload the page for clean state window.location.reload(); } catch (error) { - console.error("[SyncRecovery] Hard refresh failed:", error); + log.error('Hard refresh failed', { error }); isRefreshing = false; } } diff --git a/src/lib/components/matrix/UserProfileModal.svelte b/src/lib/components/matrix/UserProfileModal.svelte index d4a9dbb..9091c8b 100644 --- a/src/lib/components/matrix/UserProfileModal.svelte +++ b/src/lib/components/matrix/UserProfileModal.svelte @@ -3,6 +3,9 @@ import { createDirectMessage } from '$lib/matrix'; import { userPresence } from '$lib/stores/matrix'; import { toasts } from '$lib/stores/ui'; + import { createLogger } from '$lib/utils/logger'; + + const log = createLogger('matrix:profile'); import type { RoomMember } from '$lib/matrix/types'; interface Props { @@ -35,7 +38,7 @@ onStartDM?.(roomId); onClose(); } catch (e) { - console.error('Failed to start DM:', e); + log.error('Failed to start DM', { error: e }); toasts.error('Failed to start direct message'); } finally { isStartingDM = false; diff --git a/src/lib/components/modules/BudgetWidget.svelte b/src/lib/components/modules/BudgetWidget.svelte index abdd40a..dc9ca9a 100644 --- a/src/lib/components/modules/BudgetWidget.svelte +++ b/src/lib/components/modules/BudgetWidget.svelte @@ -318,7 +318,7 @@ {#if showAddItemModal} <!-- svelte-ignore a11y_no_static_element_interactions --> <div class="fixed inset-0 z-[60] bg-black/60 flex items-center justify-center p-4" onclick={() => (showAddItemModal = false)} onkeydown={(e) => e.key === 'Escape' && (showAddItemModal = false)}> - <!-- svelte-ignore a11y_no_static_element_interactions --> + <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> <div class="bg-surface rounded-2xl border border-light/10 p-5 w-full max-w-md space-y-4" onclick={(e) => e.stopPropagation()}> <h3 class="text-body font-heading text-white">{editingItem ? 'Edit' : 'Add'} Budget Item</h3> @@ -384,7 +384,7 @@ {#if showAddCategoryModal} <!-- svelte-ignore a11y_no_static_element_interactions --> <div class="fixed inset-0 z-[60] bg-black/60 flex items-center justify-center p-4" onclick={() => (showAddCategoryModal = false)} onkeydown={(e) => e.key === 'Escape' && (showAddCategoryModal = false)}> - <!-- svelte-ignore a11y_no_static_element_interactions --> + <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> <div class="bg-surface rounded-2xl border border-light/10 p-5 w-full max-w-sm space-y-4" onclick={(e) => e.stopPropagation()}> <h3 class="text-body font-heading text-white">Add Category</h3> @@ -399,10 +399,11 @@ <div class="flex gap-1.5"> {#each CATEGORY_COLORS as color} <button - class="w-6 h-6 rounded-full border-2 transition-all {newCategoryColor === color ? 'border-white scale-110' : 'border-transparent'}" - style="background-color: {color}" - onclick={() => (newCategoryColor = color)} - ></button> + class="w-6 h-6 rounded-full border-2 transition-all {newCategoryColor === color ? 'border-white scale-110' : 'border-transparent'}" + style="background-color: {color}" + onclick={() => (newCategoryColor = color)} + aria-label="Select color {color}" + ></button> {/each} </div> </div> diff --git a/src/lib/components/modules/SponsorsWidget.svelte b/src/lib/components/modules/SponsorsWidget.svelte index 199a975..ee93f91 100644 --- a/src/lib/components/modules/SponsorsWidget.svelte +++ b/src/lib/components/modules/SponsorsWidget.svelte @@ -422,7 +422,7 @@ {#if showAddSponsorModal} <!-- svelte-ignore a11y_no_static_element_interactions --> <div class="fixed inset-0 z-[60] bg-black/60 flex items-center justify-center p-4" onclick={() => (showAddSponsorModal = false)} onkeydown={(e) => e.key === 'Escape' && (showAddSponsorModal = false)}> - <!-- svelte-ignore a11y_no_static_element_interactions --> + <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> <div class="bg-surface rounded-2xl border border-light/10 p-5 w-full max-w-md space-y-4 max-h-[80vh] overflow-auto" onclick={(e) => e.stopPropagation()}> <h3 class="text-body font-heading text-white">{editingSponsor ? 'Edit' : 'Add'} Sponsor</h3> @@ -508,7 +508,7 @@ {#if showAddTierModal} <!-- svelte-ignore a11y_no_static_element_interactions --> <div class="fixed inset-0 z-[60] bg-black/60 flex items-center justify-center p-4" onclick={() => (showAddTierModal = false)} onkeydown={(e) => e.key === 'Escape' && (showAddTierModal = false)}> - <!-- svelte-ignore a11y_no_static_element_interactions --> + <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> <div class="bg-surface rounded-2xl border border-light/10 p-5 w-full max-w-sm space-y-4" onclick={(e) => e.stopPropagation()}> <h3 class="text-body font-heading text-white">Manage Tiers</h3> @@ -528,10 +528,11 @@ <div class="flex gap-1.5"> {#each TIER_COLORS as color} <button - class="w-6 h-6 rounded-full border-2 transition-all {tierColor === color ? 'border-white scale-110' : 'border-transparent'}" - style="background-color: {color}" - onclick={() => (tierColor = color)} - ></button> + class="w-6 h-6 rounded-full border-2 transition-all {tierColor === color ? 'border-white scale-110' : 'border-transparent'}" + style="background-color: {color}" + onclick={() => (tierColor = color)} + aria-label="Select color {color}" + ></button> {/each} </div> </div> diff --git a/src/lib/matrix/client.ts b/src/lib/matrix/client.ts index 77c4699..e0995bb 100644 --- a/src/lib/matrix/client.ts +++ b/src/lib/matrix/client.ts @@ -16,6 +16,9 @@ import { type PinnedEventsContent, type RoomAvatarContent, } from './sdk-types'; +import { createLogger } from '$lib/utils/logger'; + +const log = createLogger('matrix:client'); // Matrix message content types interface MessageContent extends IContent { @@ -81,7 +84,7 @@ export interface LoginWithPasswordParams { */ export async function initMatrixClient(credentials: LoginCredentials): Promise<MatrixClient> { if (client) { - console.warn('Matrix client already initialized, stopping existing client'); + log.warn('Matrix client already initialized, stopping existing client'); await stopClient(); } @@ -103,11 +106,11 @@ export async function initMatrixClient(credentials: LoginCredentials): Promise<M // Check if crypto module is available before trying to init if (typeof client.initRustCrypto === 'function') { await client.initRustCrypto(); - console.log('E2EE crypto initialized successfully'); + log.info('E2EE crypto initialized successfully'); } } catch (e) { // This is expected in dev mode - WASM loading can be problematic - console.info('Crypto not available - encrypted rooms will show encrypted messages'); + log.info('Crypto not available - encrypted rooms will show encrypted messages'); } // Start the client (begins sync loop) @@ -204,7 +207,7 @@ export async function logout(): Promise<void> { try { await client.logout(); } catch (e) { - console.error('Error during logout:', e); + log.error('Error during logout', { error: e }); } await stopClient(); } @@ -551,7 +554,7 @@ export async function setRoomNotificationLevel(roomId: string, level: Notificati } } } catch (e) { - console.error('Failed to set notification level:', e); + log.error('Failed to set notification level', { error: e }); throw e; } } @@ -628,7 +631,7 @@ export async function loadMoreMessages(roomId: string, limit = 50): Promise<{ ha return { hasMore, loaded }; } catch (e) { - console.error('Failed to load more messages:', e); + log.error('Failed to load more messages', { error: e }); return { hasMore: false, loaded: 0 }; } } @@ -697,7 +700,7 @@ export async function sendFileMessage( content.info.w = dimensions.width; content.info.h = dimensions.height; } catch (e) { - console.warn('Failed to get image dimensions:', e); + log.warn('Failed to get image dimensions', { error: e }); } } @@ -782,7 +785,7 @@ export async function getAuthenticatedMediaUrl(mxcUrl: string): Promise<string | return blobUrl; } catch (e) { - console.error('Failed to fetch authenticated media:', e); + log.error('Failed to fetch authenticated media', { error: e }); // Fallback to unauthenticated URL return client.mxcUrlToHttp(mxcUrl); } @@ -834,7 +837,7 @@ export async function getAuthenticatedThumbnailUrl( return blobUrl; } catch (e) { - console.error('Failed to fetch authenticated thumbnail:', e); + log.error('Failed to fetch authenticated thumbnail', { error: e }); return client.mxcUrlToHttp(mxcUrl, width, height, 'scale'); } } @@ -936,7 +939,7 @@ export async function searchUsers(query: string, limit = 10): Promise<Array<{ avatarUrl: user.avatar_url ? client!.mxcUrlToHttp(user.avatar_url, 40, 40, 'crop') : null, })); } catch (e) { - console.error('User search failed:', e); + log.error('User search failed', { error: e }); return []; } } @@ -1080,7 +1083,7 @@ export async function setPresence(presence: 'online' | 'offline' | 'unavailable' try { await client.setPresence({ presence, status_msg: statusMsg }); } catch (e) { - console.error('Failed to set presence:', e); + log.error('Failed to set presence', { error: e }); } } diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts index edb73b9..a884ffd 100644 --- a/src/lib/stores/theme.ts +++ b/src/lib/stores/theme.ts @@ -109,7 +109,7 @@ function loadTheme(): ThemeState { return { ...defaultTheme, ...JSON.parse(stored) }; } } catch (e) { - console.warn('Failed to load theme:', e); + // Theme load failure is non-critical, fall through to default } return defaultTheme; } diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts index e68581f..d10d807 100644 --- a/src/lib/utils/logger.ts +++ b/src/lib/utils/logger.ts @@ -195,6 +195,19 @@ export function clearRecentLogs() { * Format recent logs as a copyable string for bug reports. * User can paste this to you for debugging. */ +/** + * Safely extract an error message from an unknown caught value. + * Use in catch blocks: `catch (e: unknown) { toasts.error(getErrorMessage(e, 'fallback')) }` + */ +export function getErrorMessage(e: unknown, fallback = 'An unexpected error occurred'): string { + if (e instanceof Error) return e.message; + if (typeof e === 'string') return e; + if (e && typeof e === 'object' && 'message' in e && typeof (e as { message: unknown }).message === 'string') { + return (e as { message: string }).message; + } + return fallback; +} + export function dumpLogs(): string { return recentLogs .map((e) => { diff --git a/src/routes/[orgSlug]/chat/+page.svelte b/src/routes/[orgSlug]/chat/+page.svelte index 80050fd..e5ebc46 100644 --- a/src/routes/[orgSlug]/chat/+page.svelte +++ b/src/routes/[orgSlug]/chat/+page.svelte @@ -45,6 +45,9 @@ import { clearBlobUrlCache } from "$lib/cache/mediaCache"; import type { Message } from "$lib/matrix/types"; import type { SupabaseClient } from "@supabase/supabase-js"; + import { createLogger, getErrorMessage } from "$lib/utils/logger"; + + const log = createLogger('chat:page'); const supabase = getContext<SupabaseClient>("supabase"); let data = $derived(page.data); @@ -120,7 +123,7 @@ await initCache(); await cleanupCache(7 * 24 * 60 * 60 * 1000); } catch (e) { - console.warn("Cache initialization failed:", e); + log.warn('Cache initialization failed', { error: e }); } // Try to load credentials from Supabase @@ -141,7 +144,7 @@ isInitializing = false; } } catch (e) { - console.error("Failed to load Matrix credentials:", e); + log.error('Failed to load Matrix credentials', { error: e }); showJoinScreen = true; isInitializing = false; } @@ -164,7 +167,7 @@ // Check if org has a Matrix Space, auto-create if not await ensureOrgSpace(credentials); } catch (e: unknown) { - console.error("Failed to init Matrix client:", e); + log.error('Failed to init Matrix client', { error: e }); toasts.error(m.chat_join_error()); showJoinScreen = true; } finally { @@ -196,7 +199,7 @@ } } } catch (e) { - console.warn("Failed to ensure org space:", e); + log.warn('Failed to ensure org space', { error: e }); } } @@ -223,8 +226,8 @@ if (result.provisioned) { toasts.success(m.chat_join_success()); } - } catch (e: any) { - toasts.error(e.message || m.chat_join_error()); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, m.chat_join_error())); } finally { isProvisioning = false; } @@ -297,8 +300,8 @@ await editMessage($selectedRoomId, editingMsg.eventId, newContent); editingMsg = null; toasts.success("Message edited"); - } catch (e: any) { - toasts.error(e.message || "Failed to edit message"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to edit message')); } } @@ -312,8 +315,8 @@ try { await deleteMessage($selectedRoomId, messageId); toasts.success("Message deleted"); - } catch (e: any) { - toasts.error(e.message || "Failed to delete message"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to delete message')); } } @@ -355,8 +358,8 @@ const contentUri = await uploadFile(file); await sendFileMessage($selectedRoomId, file, contentUri); toasts.success("File sent!"); - } catch (e: any) { - toasts.error(e.message || "Failed to upload file"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to upload file')); } finally { isUploadingDrop = false; } @@ -369,8 +372,8 @@ const result = await loadMoreMessages($selectedRoomId); loadRoomMessages($selectedRoomId); if (!result.hasMore) toasts.info("No more messages to load"); - } catch (e: any) { - console.error("Failed to load more messages:", e); + } catch (e: unknown) { + log.error('Failed to load more messages', { error: e }); } finally { isLoadingMore = false; } diff --git a/src/routes/[orgSlug]/events/+page.server.ts b/src/routes/[orgSlug]/events/+page.server.ts index a5f8810..d117a49 100644 --- a/src/routes/[orgSlug]/events/+page.server.ts +++ b/src/routes/[orgSlug]/events/+page.server.ts @@ -22,7 +22,7 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { try { const events = await fetchEvents(locals.supabase, org.id, statusFilter); return { events, statusFilter }; - } catch (e: any) { + } catch (e: unknown) { log.error('Failed to load events', { error: e, data: { orgId: org.id } }); return { events: [], statusFilter }; } diff --git a/src/routes/[orgSlug]/events/+page.svelte b/src/routes/[orgSlug]/events/+page.svelte index 290b705..dd05913 100644 --- a/src/routes/[orgSlug]/events/+page.svelte +++ b/src/routes/[orgSlug]/events/+page.svelte @@ -6,6 +6,7 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; + import { getErrorMessage } from "$lib/utils/logger"; import * as m from "$lib/paraglide/messages"; interface EventItem { @@ -100,8 +101,8 @@ showCreateModal = false; resetForm(); goto(`/${data.org.slug}/events/${created.slug}`); - } catch (e: any) { - toasts.error(e.message || "Failed to create event"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to create event')); } finally { creating = false; } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts index 8d63c6e..5fad0d5 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts @@ -23,8 +23,8 @@ export const load: LayoutServerLoad = async ({ params, locals, parent }) => { ]); return { event, eventMembers: members, eventRoles: roles, eventDepartments: departments }; - } catch (e: any) { - if (e?.status === 404) throw e; + } catch (e: unknown) { + if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e; log.error('Failed to load event', { error: e, data: { orgId, eventSlug: params.eventSlug } }); error(500, 'Failed to load event'); } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte index 4de5e1e..118e9c1 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte @@ -5,6 +5,7 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; + import { getErrorMessage } from "$lib/utils/logger"; import type { Event, EventMemberWithDetails, EventRole, EventDepartment } from "$lib/api/events"; import * as m from "$lib/paraglide/messages"; @@ -173,8 +174,8 @@ goto(`/${data.org.slug}/events/${data.event.slug}`, { invalidateAll: true, }); - } catch (e: any) { - toasts.error(e.message || "Failed to update event"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to update event')); } finally { saving = false; } @@ -192,8 +193,8 @@ toasts.success(m.events_deleted()); goto(`/${data.org.slug}/events`); - } catch (e: any) { - toasts.error(e.message || "Failed to delete event"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to delete event')); } finally { deleting = false; } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.server.ts b/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.server.ts index 670f1e9..cf6f853 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.server.ts +++ b/src/routes/[orgSlug]/events/[eventSlug]/dept/[deptId]/+page.server.ts @@ -54,7 +54,7 @@ export const load: PageServerLoad = async ({ params, locals, parent }) => { sponsors, sponsorDeliverables, }; - } catch (e: any) { + } catch (e: unknown) { log.error('Failed to load department dashboard', { error: e, data: { deptId: params.deptId } }); error(500, 'Failed to load department dashboard'); } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte index d0eccc4..385b29f 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte @@ -4,6 +4,7 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; + import { getErrorMessage } from "$lib/utils/logger"; import type { Event, EventMemberWithDetails, @@ -260,8 +261,8 @@ selectedRoleId = ""; selectedDeptIds = []; addNotes = ""; - } catch (e: any) { - toasts.error(e.message || "Failed to add member"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to add member')); } finally { adding = false; } @@ -327,8 +328,8 @@ toasts.success(m.team_updated()); editingMember = null; - } catch (e: any) { - toasts.error(e.message || "Failed to update member"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to update member')); } finally { updatingMember = false; } @@ -349,8 +350,8 @@ teamMembers = teamMembers.filter((tm) => tm.id !== memberToRemove!.id); toasts.success(m.team_removed({ name })); memberToRemove = null; - } catch (e: any) { - toasts.error(e.message || "Failed to remove member"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to remove member')); } finally { removing = false; } @@ -398,8 +399,8 @@ toasts.success(m.team_dept_created()); } showDeptModal = false; - } catch (e: any) { - toasts.error(e.message || "Failed to save department"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to save department')); } finally { savingDept = false; } @@ -419,8 +420,8 @@ departments: tm.departments.filter((d) => d.id !== dept.id), })); toasts.success(m.team_dept_deleted()); - } catch (e: any) { - toasts.error(e.message || "Failed to delete department"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to delete department')); } } @@ -464,8 +465,8 @@ toasts.success(m.team_role_created()); } showRoleModal = false; - } catch (e: any) { - toasts.error(e.message || "Failed to save role"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to save role')); } finally { savingRole = false; } @@ -486,8 +487,8 @@ : tm, ); toasts.success(m.team_role_deleted()); - } catch (e: any) { - toasts.error(e.message || "Failed to delete role"); + } catch (e: unknown) { + toasts.error(getErrorMessage(e, 'Failed to delete role')); } } diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 5680cfa..451892f 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -621,8 +621,9 @@ <Input label="Name" name="name" bind:value={editEventModal.name} /> <Input label="Slug" name="slug" bind:value={editEventModal.slug} /> <div> - <label class="block text-body-sm text-light/60 mb-1">Status</label> + <label for="event-status" class="block text-body-sm text-light/60 mb-1">Status</label> <select + id="event-status" name="status" bind:value={editEventModal.status} class="w-full bg-dark/50 border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary/50" @@ -634,8 +635,9 @@ </div> <div class="grid grid-cols-2 gap-3"> <div> - <label class="block text-body-sm text-light/60 mb-1">Start Date</label> + <label for="event-start-date" class="block text-body-sm text-light/60 mb-1">Start Date</label> <input + id="event-start-date" type="date" name="start_date" bind:value={editEventModal.start_date} @@ -643,8 +645,9 @@ /> </div> <div> - <label class="block text-body-sm text-light/60 mb-1">End Date</label> + <label for="event-end-date" class="block text-body-sm text-light/60 mb-1">End Date</label> <input + id="event-end-date" type="date" name="end_date" bind:value={editEventModal.end_date} diff --git a/src/routes/api/matrix-provision/+server.ts b/src/routes/api/matrix-provision/+server.ts index c7040e8..0eaedc0 100644 --- a/src/routes/api/matrix-provision/+server.ts +++ b/src/routes/api/matrix-provision/+server.ts @@ -1,6 +1,9 @@ import { json } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import type { RequestHandler } from './$types'; +import { createLogger } from '$lib/utils/logger'; + +const log = createLogger('api:matrix-provision'); /** * POST /api/matrix-provision @@ -83,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { if (!registerRes.ok) { const err = await registerRes.json().catch(() => ({})); - console.error('Matrix register failed:', registerRes.status, err); + log.error('Matrix register failed', { data: { status: registerRes.status }, error: err }); return json({ error: 'Failed to create Matrix account' }, { status: 500 }); } @@ -104,7 +107,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { if (!loginRes.ok) { const err = await loginRes.json().catch(() => ({})); - console.error('Matrix login failed:', loginRes.status, err); + log.error('Matrix login failed', { data: { status: loginRes.status }, error: err }); return json({ error: 'Failed to login to Matrix account' }, { status: 500 }); } @@ -118,7 +121,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { try { await setMatrixAvatar(homeserverUrl, accessToken, profile.avatar_url); } catch (e) { - console.warn('Failed to set Matrix avatar:', e); + log.warn('Failed to set Matrix avatar', { error: e }); } } @@ -138,7 +141,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { ); if (upsertError) { - console.error('Failed to store Matrix credentials:', upsertError); + log.error('Failed to store Matrix credentials', { error: upsertError }); return json({ error: 'Failed to store credentials' }, { status: 500 }); } @@ -152,7 +155,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { provisioned: true, }); } catch (e) { - console.error('Matrix provisioning error:', e); + log.error('Matrix provisioning error', { error: e }); return json({ error: 'Matrix provisioning failed' }, { status: 500 }); } }; diff --git a/src/routes/api/matrix-space/+server.ts b/src/routes/api/matrix-space/+server.ts index b90fdfe..1107ced 100644 --- a/src/routes/api/matrix-space/+server.ts +++ b/src/routes/api/matrix-space/+server.ts @@ -1,5 +1,8 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +import { createLogger, getErrorMessage } from '$lib/utils/logger'; + +const log = createLogger('api:matrix-space'); /** * GET: Retrieve the Matrix Space ID for an org @@ -140,9 +143,9 @@ export const POST: RequestHandler = async ({ request, locals }) => { } return json({ spaceId, created: true }); - } catch (e: any) { - console.error('Failed to create Matrix Space:', e); - return json({ error: e.message || 'Failed to create Matrix Space' }, { status: 500 }); + } catch (e: unknown) { + log.error('Failed to create Matrix Space', { error: e }); + return json({ error: getErrorMessage(e, 'Failed to create Matrix Space') }, { status: 500 }); } }