From 85bf00b3a939ccb3ef813b484158ee1210ddb403 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 11 Oct 2026 17:42:54 +0200 Subject: [PATCH 1/2] refactor: frontend on SvelteKit 3 with adapter-static and Vite SvelteKit's router, layout guards, error page and svelte:head replace the svelte5-router setup, main.ts, App.svelte, the error store and meta.ts; Vite replaces the Rollup config and its hand-written dev server. Imports move to package.json subpath imports since SvelteKit 3 dropped $lib. The CSP is a meta tag Kit hashes into 200.html, so nginx stops injecting nonces, and the generated schemas use zod/mini, which never probes eval. Dropping the global error handlers stops a failed API call in a click handler from replacing the app. Closes #325 Closes #291 --- .gitignore | 3 +- AGENTS.md | 73 +- docs/SECURITY.md | 2 +- docs/architecture/frontend-build.md | 46 +- docs/frontend/error-handling.md | 14 +- docs/frontend/routing.md | 63 +- docs/operations/nginx-configuration.md | 14 +- docs/testing/frontend-testing.md | 14 +- frontend/.dockerignore | 1 - frontend/.gitignore | 4 +- frontend/Dockerfile | 2 +- frontend/nginx.conf.template | 12 +- frontend/openapi-ts.config.ts | 2 +- frontend/package-lock.json | 8758 ++++++----------- frontend/package.json | 45 +- frontend/postcss.config.cjs | 10 - frontend/rollup.config.js | 246 - frontend/src/App.svelte | 113 - frontend/src/__tests__/hooks.client.test.ts | 19 + frontend/src/__tests__/test-utils.ts | 4 +- frontend/src/app.html | 23 + frontend/src/components/Footer.svelte | 2 +- frontend/src/components/Header.svelte | 26 +- .../src/components/NotificationCenter.svelte | 16 +- .../components/__tests__/ErrorDisplay.test.ts | 4 +- .../__tests__/EventTypeIcon.test.ts | 2 +- .../src/components/__tests__/Footer.test.ts | 4 +- .../src/components/__tests__/Header.test.ts | 20 +- .../src/components/__tests__/Modal.test.ts | 2 +- .../components/__tests__/ModalWrapper.svelte | 2 +- .../__tests__/NotificationCenter.test.ts | 12 +- .../components/__tests__/Pagination.test.ts | 4 +- .../src/components/__tests__/Spinner.test.ts | 2 +- .../admin/AutoRefreshControl.svelte | 2 +- .../__tests__/AutoRefreshControl.test.ts | 8 +- .../admin/__tests__/FilterPanel.test.ts | 2 +- .../admin/__tests__/FilterPanelWrapper.svelte | 2 +- .../admin/events/EventDetailsModal.svelte | 10 +- .../admin/events/EventFilters.svelte | 4 +- .../admin/events/EventStatsCards.svelte | 2 +- .../admin/events/EventsTable.svelte | 8 +- .../admin/events/ReplayPreviewModal.svelte | 6 +- .../admin/events/ReplayProgressBanner.svelte | 2 +- .../admin/events/UserOverviewModal.svelte | 10 +- .../__tests__/EventDetailsModal.test.ts | 4 +- .../events/__tests__/EventFilters.test.ts | 8 +- .../events/__tests__/EventStatsCards.test.ts | 4 +- .../events/__tests__/EventsTable.test.ts | 4 +- .../__tests__/ReplayPreviewModal.test.ts | 4 +- .../__tests__/ReplayProgressBanner.test.ts | 8 +- .../__tests__/UserOverviewModal.test.ts | 4 +- frontend/src/components/admin/events/index.ts | 14 +- frontend/src/components/admin/index.ts | 4 +- .../admin/sagas/SagaDetailsModal.svelte | 8 +- .../components/admin/sagas/SagaFilters.svelte | 2 +- .../admin/sagas/SagaStatsCards.svelte | 4 +- .../components/admin/sagas/SagasTable.svelte | 8 +- .../sagas/__tests__/SagaDetailsModal.test.ts | 10 +- .../admin/sagas/__tests__/SagaFilters.test.ts | 6 +- .../sagas/__tests__/SagaStatsCards.test.ts | 8 +- .../admin/sagas/__tests__/SagasTable.test.ts | 14 +- frontend/src/components/admin/sagas/index.ts | 8 +- .../admin/users/DeleteUserModal.svelte | 6 +- .../admin/users/RateLimitsModal.svelte | 10 +- .../components/admin/users/UserFilters.svelte | 4 +- .../admin/users/UserFormModal.svelte | 8 +- .../components/admin/users/UsersTable.svelte | 6 +- .../users/__tests__/DeleteUserModal.test.ts | 8 +- .../users/__tests__/RateLimitsModal.test.ts | 12 +- .../admin/users/__tests__/UserFilters.test.ts | 6 +- .../users/__tests__/UserFormModal.test.ts | 10 +- .../admin/users/__tests__/UsersTable.test.ts | 8 +- frontend/src/components/admin/users/index.ts | 10 +- .../components/editor/CodeMirrorEditor.svelte | 11 +- .../components/editor/LanguageSelect.svelte | 4 +- .../src/components/editor/OutputPanel.svelte | 6 +- .../components/editor/ResourceLimits.svelte | 2 +- .../src/components/editor/SavedScripts.svelte | 2 +- .../editor/__tests__/CodeMirrorEditor.test.ts | 8 +- .../editor/__tests__/EditorToolbar.test.ts | 4 +- .../editor/__tests__/LanguageSelect.test.ts | 6 +- .../editor/__tests__/OutputPanel.test.ts | 8 +- .../editor/__tests__/ResourceLimits.test.ts | 4 +- .../editor/__tests__/SavedScripts.test.ts | 6 +- .../editor/__tests__/ScriptActions.test.ts | 4 +- frontend/src/components/editor/index.ts | 14 +- frontend/src/hooks.client.ts | 13 + frontend/src/index.html | 51 - .../src/lib/__mocks__/api-interceptors.ts | 2 +- .../lib/__tests__/api-interceptors.test.ts | 12 +- frontend/src/lib/__tests__/auth-guard.test.ts | 64 + frontend/src/lib/__tests__/formatters.test.ts | 2 +- .../src/lib/__tests__/user-settings.test.ts | 46 +- frontend/src/lib/__tests__/validation.test.ts | 6 +- .../src/lib/admin/__tests__/constants.test.ts | 2 +- .../lib/admin/__tests__/pagination.test.ts | 2 +- .../admin/events/__tests__/eventTypes.test.ts | 14 +- frontend/src/lib/admin/events/eventTypes.ts | 4 +- frontend/src/lib/admin/events/index.ts | 2 +- frontend/src/lib/admin/index.ts | 10 +- .../rate-limits/__tests__/rateLimits.test.ts | 4 +- frontend/src/lib/admin/rate-limits/index.ts | 2 +- .../src/lib/admin/rate-limits/rateLimits.ts | 2 +- .../admin/sagas/__tests__/sagaStates.test.ts | 2 +- frontend/src/lib/admin/sagas/index.ts | 2 +- frontend/src/lib/admin/sagas/sagaStates.ts | 2 +- .../stores/__tests__/eventsStore.test.ts | 6 +- .../stores/__tests__/executionsStore.test.ts | 6 +- .../admin/stores/__tests__/sagasStore.test.ts | 6 +- .../lib/admin/stores/eventsStore.svelte.ts | 10 +- .../admin/stores/executionsStore.svelte.ts | 8 +- .../src/lib/admin/stores/sagasStore.svelte.ts | 10 +- frontend/src/lib/api-interceptors.ts | 10 +- frontend/src/lib/api/zod.gen.ts | 1454 +-- frontend/src/lib/auth-guard.ts | 26 + .../lib/editor/__tests__/execution.test.ts | 6 +- .../lib/editor/__tests__/languages.test.ts | 2 +- .../src/lib/editor/__tests__/runtimes.test.ts | 2 +- frontend/src/lib/editor/execution.svelte.ts | 4 +- frontend/src/lib/editor/index.ts | 6 +- frontend/src/lib/logger.ts | 5 +- .../notifications/__tests__/stream.test.ts | 6 +- .../src/lib/notifications/stream.svelte.ts | 4 +- frontend/src/lib/user-settings.ts | 8 +- frontend/src/lib/validation.ts | 8 +- frontend/src/main.ts | 45 - frontend/src/routes/(auth)/+layout.ts | 6 + .../editor/+page.svelte} | 27 +- .../notifications/+page.svelte} | 8 +- .../settings/+page.svelte} | 22 +- frontend/src/routes/+error.svelte | 13 + frontend/src/routes/+layout.svelte | 43 + frontend/src/routes/+layout.ts | 2 + .../src/routes/{Home.svelte => +page.svelte} | 12 +- frontend/src/routes/__tests__/Editor.test.ts | 37 +- frontend/src/routes/__tests__/Home.test.ts | 10 +- frontend/src/routes/__tests__/Login.test.ts | 27 +- .../routes/__tests__/Notifications.test.ts | 6 +- frontend/src/routes/__tests__/Privacy.test.ts | 2 +- .../src/routes/__tests__/Register.test.ts | 23 +- .../src/routes/__tests__/Settings.test.ts | 12 +- frontend/src/routes/__tests__/error.test.ts | 21 + frontend/src/routes/admin/+layout.svelte | 54 + frontend/src/routes/admin/+layout.ts | 6 + frontend/src/routes/admin/+page.ts | 5 + frontend/src/routes/admin/AdminEvents.svelte | 256 - .../src/routes/admin/AdminExecutions.svelte | 224 - frontend/src/routes/admin/AdminLayout.svelte | 81 - frontend/src/routes/admin/AdminSagas.svelte | 122 - .../src/routes/admin/AdminSettings.svelte | 329 - .../admin/__tests__/AdminEvents.test.ts | 11 +- .../admin/__tests__/AdminExecutions.test.ts | 11 +- .../admin/__tests__/AdminLayout.test.ts | 21 +- .../routes/admin/__tests__/AdminSagas.test.ts | 21 +- .../admin/__tests__/AdminSettings.test.ts | 10 +- .../routes/admin/__tests__/AdminUsers.test.ts | 15 +- .../__tests__/mocks/MockAdminLayout.svelte | 14 - frontend/src/routes/admin/events/+page.svelte | 253 + .../src/routes/admin/executions/+page.svelte | 213 + frontend/src/routes/admin/sagas/+page.svelte | 119 + .../src/routes/admin/settings/+page.svelte | 326 + .../{AdminUsers.svelte => users/+page.svelte} | 136 +- .../{Login.svelte => login/+page.svelte} | 24 +- .../{Privacy.svelte => privacy/+page.svelte} | 10 +- .../+page.svelte} | 25 +- frontend/src/stores/__tests__/auth.test.ts | 70 +- .../src/stores/__tests__/errorStore.test.ts | 95 - .../__tests__/notificationStore.test.ts | 58 +- frontend/src/stores/__tests__/theme.test.ts | 36 +- .../src/stores/__tests__/userSettings.test.ts | 15 +- frontend/src/stores/auth.svelte.ts | 8 +- frontend/src/stores/errorStore.svelte.ts | 23 - .../src/stores/notificationStore.svelte.ts | 4 +- frontend/src/stores/theme.svelte.ts | 14 +- frontend/src/stores/userSettings.svelte.ts | 2 +- frontend/src/utils/__tests__/meta.test.ts | 145 - frontend/src/utils/meta.ts | 46 - frontend/{public => static}/favicon.png | Bin frontend/{public => static}/robots.txt | 0 frontend/{public => static}/sitemap.xml | 0 frontend/svelte.config.js | 6 - frontend/tsconfig.json | 36 +- frontend/vite.config.ts | 50 + frontend/vitest.config.ts | 13 +- frontend/vitest.setup.ts | 8 +- 185 files changed, 5564 insertions(+), 9407 deletions(-) delete mode 100644 frontend/postcss.config.cjs delete mode 100644 frontend/rollup.config.js delete mode 100644 frontend/src/App.svelte create mode 100644 frontend/src/__tests__/hooks.client.test.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/hooks.client.ts delete mode 100644 frontend/src/index.html create mode 100644 frontend/src/lib/__tests__/auth-guard.test.ts create mode 100644 frontend/src/lib/auth-guard.ts delete mode 100644 frontend/src/main.ts create mode 100644 frontend/src/routes/(auth)/+layout.ts rename frontend/src/routes/{Editor.svelte => (auth)/editor/+page.svelte} (95%) rename frontend/src/routes/{Notifications.svelte => (auth)/notifications/+page.svelte} (98%) rename frontend/src/routes/{Settings.svelte => (auth)/settings/+page.svelte} (97%) create mode 100644 frontend/src/routes/+error.svelte create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+layout.ts rename frontend/src/routes/{Home.svelte => +page.svelte} (95%) create mode 100644 frontend/src/routes/__tests__/error.test.ts create mode 100644 frontend/src/routes/admin/+layout.svelte create mode 100644 frontend/src/routes/admin/+layout.ts create mode 100644 frontend/src/routes/admin/+page.ts delete mode 100644 frontend/src/routes/admin/AdminEvents.svelte delete mode 100644 frontend/src/routes/admin/AdminExecutions.svelte delete mode 100644 frontend/src/routes/admin/AdminLayout.svelte delete mode 100644 frontend/src/routes/admin/AdminSagas.svelte delete mode 100644 frontend/src/routes/admin/AdminSettings.svelte delete mode 100644 frontend/src/routes/admin/__tests__/mocks/MockAdminLayout.svelte create mode 100644 frontend/src/routes/admin/events/+page.svelte create mode 100644 frontend/src/routes/admin/executions/+page.svelte create mode 100644 frontend/src/routes/admin/sagas/+page.svelte create mode 100644 frontend/src/routes/admin/settings/+page.svelte rename frontend/src/routes/admin/{AdminUsers.svelte => users/+page.svelte} (78%) rename frontend/src/routes/{Login.svelte => login/+page.svelte} (89%) rename frontend/src/routes/{Privacy.svelte => privacy/+page.svelte} (98%) rename frontend/src/routes/{Register.svelte => register/+page.svelte} (91%) delete mode 100644 frontend/src/stores/__tests__/errorStore.test.ts delete mode 100644 frontend/src/stores/errorStore.svelte.ts delete mode 100644 frontend/src/utils/__tests__/meta.test.ts delete mode 100644 frontend/src/utils/meta.ts rename frontend/{public => static}/favicon.png (100%) rename frontend/{public => static}/robots.txt (100%) rename frontend/{public => static}/sitemap.xml (100%) delete mode 100644 frontend/svelte.config.js create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index ee489c0ae..2f0140704 100644 --- a/.gitignore +++ b/.gitignore @@ -92,7 +92,8 @@ yarn-debug.log* yarn-error.log* # Build outputs -frontend/public/build/ +frontend/build/ +frontend/.svelte-kit/ # Frontend testing frontend/coverage/ diff --git a/AGENTS.md b/AGENTS.md index 70c816931..4f62fc52f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Platform for running scripts online in isolated Kubernetes pods. Users write Pyt | Layer | Technology | |-------|-----------| -| Frontend | Svelte 5 (runes), TypeScript 6 strict, Rollup, TailwindCSS 4, Node 26 | +| Frontend | Svelte 5 (runes), SvelteKit 3 (adapter-static SPA) on Vite 8, TypeScript 6 strict, TailwindCSS 4, Node 26 | | Backend | FastAPI, Dishka DI, Beanie ODM (MongoDB), Python 3.14 (backend/.python-version), async throughout, httpx2 for HTTP | | Events | Kafka (KRaft, no ZooKeeper) + FastStream (Pydantic JSON) | | Workers | k8s_worker, pod_monitor, result_processor, saga_orchestrator, dlq_processor, event_replay | @@ -38,15 +38,19 @@ backend/ frontend/ ├── e2e/ # Playwright specs and fixtures.ts +├── static/ # favicon, robots.txt, sitemap.xml, copied into the build as is +├── vite.config.ts # SvelteKit plugin (adapter-static, CSP), Tailwind, dev server └── src/ - ├── routes/ # Page components (admin/ pages included) + ├── app.html # Page template + ├── hooks.client.ts # init (API interceptors) and handleError + ├── routes/ # SvelteKit routes: +page.svelte per page, (auth)/ and admin/ guarded by +layout.ts ├── components/ # Reusable UI: admin/, editor/, plus shared top-level components ├── lib/api/ # Generated API client, never edited by hand (see Frontend: API Client) ├── lib/admin/ # Admin utilities: events/, rate-limits/, sagas/, stores/ ├── lib/editor/ # Execution state, languages, runtime helpers (newestVersion) ├── lib/notifications/ # Notification SSE stream - ├── stores/ # auth, errorStore, notificationStore, theme, userSettings (.svelte.ts) - ├── utils/ # meta.ts (page meta tags) + ├── lib/auth-guard.ts # requireAuth and requireAdmin for the layout guards + ├── stores/ # auth, notificationStore, theme, userSettings (.svelte.ts) └── styles/ # Global CSS ``` @@ -63,7 +67,7 @@ uv run python -m scripts.check_orphan_modules # dead code (must pass) # Frontend (from frontend/) npm ci # install deps -npm run dev # watch build + HTTPS server on :5001 (see note below) +npm run dev # Vite dev server with HMR on :5001 (see note below) npm run build # production build npm run check # svelte-check, 0 errors and 0 warnings required npm run lint # ESLint @@ -85,7 +89,7 @@ npm run test:e2e # Playwright E2E, needs the running stack ./deploy.sh types # regenerate frontend/src/lib/api/ from that spec ``` -`npm run dev` proxies /api to `backend:443` and needs the CA at /shared_ca/ca.pem, both of which exist only inside the compose network, so on the host it exits. The compose `frontend` service already serves a production build on :5001; rebuild it with `./deploy.sh dev --build` after frontend changes. +`npm run dev` serves HTTPS when ./certs/server.key and server.crt exist and proxies /api to `https://backend:443`, a hostname that only resolves inside the compose network, so on the host the API calls fail. The compose `frontend` service already serves a production build on :5001; rebuild it with `./deploy.sh dev --build` after frontend changes. Plain `uv run pytest` also collects tests/e2e, which needs the full stack, so always pass a path. E2E tests run inside the backend container against a running stack, the way CI does it (CI copies config.test.toml over config.toml before starting the stack): @@ -455,7 +459,7 @@ class ExecutionService: - `Settings.SUPPORTED_RUNTIMES` and `ExecutionRequest` validation both derive from `LANGUAGE_SPECS`. Removing a version makes new executions, retries and saved-script writes on it fail with 422; stored scripts still load - Per-language pod settings go in the spec's optional `pod` key as a `PodRuntimeConfig` (env, tmp_size, min_cpu, min_memory, min_timeout_seconds). `POD_RUNTIME_CONFIGS` exposes them and the pod builder raises limits to those minimums. Only Go sets one today - Scripts mount at `/scripts/main.`. Every language needs an `EXAMPLE_SCRIPTS` entry, and the Python example must parse on every offered version -- The frontend picks the newest version with `newestVersion()` from `$lib/editor/runtimes`, so it doesn't rely on API ordering +- The frontend picks the newest version with `newestVersion()` from `#lib/editor/runtimes.ts`, so it doesn't rely on API ordering ## Backend: Settings (TOML, secrets from env) @@ -611,17 +615,17 @@ Strict mode plus extra strictness in `tsconfig.json`: - `"noImplicitReturns": true` - `"verbatimModuleSyntax": true`: use `import type` for type-only imports -Path aliases (always use them, never relative `../../lib/`): +Imports go through the package.json subpath imports (SvelteKit 3 dropped `$lib`), always with the file extension; never relative `../../lib/`: ```text -$lib = src/lib -$components = src/components -$stores = src/stores -$routes = src/routes -$utils = src/utils -$styles = src/styles -$test = src/__tests__ +#lib/* = src/lib/* e.g. #lib/api/index.ts, #lib/formatters.ts +#components/* = src/components/* e.g. #components/Header.svelte +#stores/* = src/stores/* e.g. #stores/auth.svelte.ts +#routes/* = src/routes/* e.g. #routes/admin/users/+page.svelte (tests) +#test/* = src/__tests__/* e.g. #test/test-utils.ts ``` +`tsconfig.json` extends `$app/tsconfig`, which `svelte-kit sync` generates; `npm run check` and `npm run lint` run it first. + Use `interface` for object shapes, `type` for unions/intersections/aliases. Define prop interfaces explicitly before `$props()`. ## Frontend: API Client @@ -637,10 +641,10 @@ Hard rule: `docs/reference/openapi.json` and everything under `frontend/src/lib/ Function names are derived from URL paths: `getUserRateLimitsApiV1AdminRateLimitsUserIdGet`. -Anything that comes from the backend comes from `$lib/api`, never from a hand-written copy: types, enum values, validation rules, defaults, URLs and SSE streams. The generator emits runtime enums and a Zod schema per model, and the index re-exports the client and the schemas, so the copies have nothing to add and only drift. +Anything that comes from the backend comes from `#lib/api/index.ts`, never from a hand-written copy: types, enum values, validation rules, defaults, URLs and SSE streams. The generator emits runtime enums and a Zod schema per model, and the index re-exports the client and the schemas, so the copies have nothing to add and only drift. ```typescript -import { UserRole, zUserCreate, client, type ExportEventsApiV1AdminEventsExportExportFormatGetData } from '$lib/api'; +import { UserRole, zUserCreate, client, type ExportEventsApiV1AdminEventsExportExportFormatGetData } from '#lib/api/index.ts'; Object.values(UserRole); // option lists, never a literal array zUserCreate.safeParse(form); // the spec's rules (email format, minLength) @@ -652,8 +656,8 @@ client.buildUrl({ url: '/ UI-only maps keyed by a generated enum (`Record` for badge colors) are fine, since the compiler flags a missing value. ```typescript -import { someApiEndpoint } from '$lib/api'; -import { unwrap, unwrapOr } from '$lib/api-interceptors'; +import { someApiEndpoint } from '#lib/api/index.ts'; +import { unwrap, unwrapOr } from '#lib/api-interceptors.ts'; // unwrap: throws on error, use in event handlers (the interceptor shows the toast) const data = unwrap(await someApiEndpoint({ body: payload })); @@ -683,32 +687,26 @@ For the login and register endpoints the interceptor skips only the 401, network ## Frontend: Auth Store -- Auth state lives in `$stores/auth.svelte.ts` and persists to `sessionStorage` under one key, `authState`, as a JSON blob (`isAuthenticated`, `username`, `userId`, `userRole`, `userEmail`, `csrfToken`, `timestamp`) +- Auth state lives in `#stores/auth.svelte.ts` and persists to `sessionStorage` under one key, `authState`, as a JSON blob (`isAuthenticated`, `username`, `userId`, `userRole`, `userEmail`, `csrfToken`, `timestamp`) - `login(username, password): Promise` throws the API error on failure and returns `false` if the follow-up profile check fails - `verifyAuth()` has a 30-second cache; on a network error it returns the cached result (offline-first) - `authStore.waitForInit()` awaits one-time initialization before checking auth ## Frontend: Routing -Uses `@mateothegreat/svelte5-router`. Routes are defined in `App.svelte`: +SvelteKit's filesystem router: a page is `src/routes//+page.svelte`, and the root `+layout.ts` sets `ssr = false`, so it's a client-rendered SPA served through adapter-static's `200.html` fallback. Guards are `load` functions, which run before the page mounts: + ```typescript -const requireAuth = async () => { - await authStore.waitForInit(); - if (!authStore.isAuthenticated) { - const currentPath = window.location.pathname + window.location.search; - if (currentPath !== '/login' && currentPath !== '/register') { - sessionStorage.setItem('redirectAfterLogin', currentPath); - } - goto('/login'); - return false; - } - return true; -}; +// src/routes/(auth)/+layout.ts guards /editor, /settings, /notifications; admin/+layout.ts uses requireAdmin +import { requireAuth } from '#lib/auth-guard.ts'; +import type { LayoutLoad } from './$types'; -{ path: '/editor', component: Editor, hooks: { pre: requireAuth } } +export const load: LayoutLoad = async ({ url }) => { + await requireAuth(url); +}; ``` -Navigation: `goto('/path')` programmatically, the `use:route` directive on links. +Navigation: plain `` for links (SvelteKit handles them), `goto('/path')` from `$app/navigation` in code, and `page` from `$app/state` for the current URL. Page titles go in `` as `Page - Integr8sCode`. Errors while loading or rendering show `src/routes/+error.svelte`. ## Testing @@ -760,13 +758,14 @@ const mocks = vi.hoisted(() => ({ someApiFunction: vi.fn(), })); -vi.mock('$lib/api', () => ({ +vi.mock('#lib/api/index.ts', async (importOriginal) => ({ + ...(await importOriginal()), someApiFunction: (...args: unknown[]) => mocks.someApiFunction(...args), })); ``` - Tests sit next to the source in `__tests__/` directories -- `vitest.setup.ts` already mocks `$lib/api` (SDK functions only; enums, Zod schemas and the client stay real), `$lib/api-interceptors`, `svelte-sonner` and the router, stubs storage, and runs `cleanup()` before every test; override a mock per file with `vi.mock`, and spread `importOriginal()` into a `$lib/api` or `$lib/formatters` factory so the real exports stay available +- `vitest.setup.ts` already mocks `#lib/api/index.ts` (SDK functions only; enums, Zod schemas and the client stay real), `#lib/api-interceptors.ts`, `svelte-sonner` and `$app/navigation`, stubs storage, and runs `cleanup()` before every test; override a mock per file with `vi.mock`, spread `importOriginal()` into a `#lib/api/index.ts` or `#lib/formatters.ts` factory so the real exports stay available, and mock `$app/state` in the file when a component reads `page` - `vi.clearAllMocks()` in `beforeEach` - `await tick()` after render, `waitFor()` for async state - Coverage thresholds: statements 92, branches 76, functions 90, lines 92; the generated client (`src/lib/api/`) is excluded diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 2154165ad..9c721ec2f 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -12,7 +12,7 @@ Include what you can: the kind of issue, where it is, steps to reproduce, a PoC CI runs Bandit on the backend, Trivy on every image that passes the tests on `main`, and Syft plus Grype on the backend and frontend dependencies. Dependabot keeps Python, npm, Docker, compose and GitHub Actions dependencies current, and every action is pinned to a commit SHA. See [Supply chain security](security/supply-chain.md). -nginx sends a nonce-based Content Security Policy for the app: scripts must be same-origin or carry the per-request nonce, inline ` - - - -
-
- - Loading… -
-
- - - diff --git a/frontend/src/lib/__mocks__/api-interceptors.ts b/frontend/src/lib/__mocks__/api-interceptors.ts index edfb04c3d..ef306aa97 100644 --- a/frontend/src/lib/__mocks__/api-interceptors.ts +++ b/frontend/src/lib/__mocks__/api-interceptors.ts @@ -4,4 +4,4 @@ import { vi } from 'vitest'; export const initializeApiInterceptors = vi.fn(); // Re-export real pure functions - no need to mock these -export { getErrorMessage, unwrap, unwrapOr } from '$lib/api-interceptors'; +export { getErrorMessage, unwrap, unwrapOr } from '#lib/api-interceptors.ts'; diff --git a/frontend/src/lib/__tests__/api-interceptors.test.ts b/frontend/src/lib/__tests__/api-interceptors.test.ts index 56f10d0dd..fc5bcd8d7 100644 --- a/frontend/src/lib/__tests__/api-interceptors.test.ts +++ b/frontend/src/lib/__tests__/api-interceptors.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const { getErrorMessage, unwrap, unwrapOr } = - await vi.importActual('$lib/api-interceptors'); + await vi.importActual('#lib/api-interceptors.ts'); describe('getErrorMessage', () => { it.each([ @@ -111,7 +111,7 @@ describe('initializeApiInterceptors', () => { mockGoto = vi.fn(); mockAuthStore = { isAuthenticated: true, csrfToken: 'csrf-123', clearAuth: vi.fn() }; - vi.doMock('$lib/api', () => ({ + vi.doMock('#lib/api/index.ts', () => ({ client: { setConfig: mockSetConfig, interceptors: { @@ -121,11 +121,11 @@ describe('initializeApiInterceptors', () => { }, })); vi.doMock('svelte-sonner', () => ({ toast: mockToast })); - vi.doMock('@mateothegreat/svelte5-router', () => ({ goto: mockGoto })); - vi.doMock('$stores/auth.svelte', () => ({ authStore: mockAuthStore })); - vi.doUnmock('$lib/api-interceptors'); + vi.doMock('$app/navigation', () => ({ goto: mockGoto })); + vi.doMock('#stores/auth.svelte.ts', () => ({ authStore: mockAuthStore })); + vi.doUnmock('#lib/api-interceptors.ts'); - const mod = await import('$lib/api-interceptors'); + const mod = await import('#lib/api-interceptors.ts'); mod.initializeApiInterceptors(); errorInterceptor = mockErrorUse.mock.calls[0]![0]; diff --git a/frontend/src/lib/__tests__/auth-guard.test.ts b/frontend/src/lib/__tests__/auth-guard.test.ts new file mode 100644 index 000000000..1bf777a80 --- /dev/null +++ b/frontend/src/lib/__tests__/auth-guard.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { isRedirect } from '@sveltejs/kit'; +import { toast } from 'svelte-sonner'; +import { requireAdmin, requireAuth } from '#lib/auth-guard.ts'; + +const mocks = vi.hoisted(() => ({ + authStore: { + isAuthenticated: true as boolean | null, + userRole: 'admin' as string | null, + waitForInit: vi.fn(), + }, +})); + +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.authStore })); + +const url = new URL('https://localhost/editor?lang=go'); + +async function redirectFrom(guard: Promise): Promise<{ status: number; location: string }> { + const thrown: unknown = await guard.catch((e: unknown) => e); + if (!isRedirect(thrown)) throw new Error('expected a redirect'); + return thrown; +} + +describe('auth guards', () => { + beforeEach(() => { + vi.clearAllMocks(); + sessionStorage.clear(); + mocks.authStore.isAuthenticated = true; + mocks.authStore.userRole = 'admin'; + mocks.authStore.waitForInit.mockResolvedValue(true); + }); + + it('lets a signed-in user through', async () => { + await expect(requireAuth(url)).resolves.toBeUndefined(); + }); + + it('sends a signed-out visitor to /login and remembers the path', async () => { + mocks.authStore.isAuthenticated = false; + mocks.authStore.waitForInit.mockResolvedValue(false); + + const redirect = await redirectFrom(requireAuth(url)); + + expect(redirect.location).toBe('/login'); + expect(sessionStorage.getItem('redirectAfterLogin')).toBe('/editor?lang=go'); + }); + + it('treats a failed auth check as signed out', async () => { + mocks.authStore.waitForInit.mockRejectedValue(new Error('down')); + + expect((await redirectFrom(requireAuth(url))).location).toBe('/login'); + }); + + it('lets an admin into admin pages', async () => { + await expect(requireAdmin(url)).resolves.toBeUndefined(); + expect(toast.error).not.toHaveBeenCalled(); + }); + + it('sends a non-admin home with a toast', async () => { + mocks.authStore.userRole = 'user'; + + expect((await redirectFrom(requireAdmin(url))).location).toBe('/'); + expect(toast.error).toHaveBeenCalledWith('Admin access required'); + }); +}); diff --git a/frontend/src/lib/__tests__/formatters.test.ts b/frontend/src/lib/__tests__/formatters.test.ts index cb2d7937c..2f5d2b2dd 100644 --- a/frontend/src/lib/__tests__/formatters.test.ts +++ b/frontend/src/lib/__tests__/formatters.test.ts @@ -8,7 +8,7 @@ import { formatRelativeTime, truncate, formatEnumLabel, -} from '$lib/formatters'; +} from '#lib/formatters.ts'; const LOCALE_OPTS_DATETIME: Intl.DateTimeFormatOptions = { year: 'numeric', diff --git a/frontend/src/lib/__tests__/user-settings.test.ts b/frontend/src/lib/__tests__/user-settings.test.ts index b4203070c..5d5d378eb 100644 --- a/frontend/src/lib/__tests__/user-settings.test.ts +++ b/frontend/src/lib/__tests__/user-settings.test.ts @@ -3,8 +3,8 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; const mockGetUserSettings = vi.fn(); const mockUpdateUserSettings = vi.fn(); -vi.mock('$lib/api', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('#lib/api/index.ts', async (importOriginal) => ({ + ...(await importOriginal()), getUserSettingsApiV1UserSettingsGet: (...args: unknown[]) => (mockGetUserSettings as (...a: unknown[]) => unknown)(...args), updateUserSettingsApiV1UserSettingsPut: (...args: unknown[]) => @@ -13,13 +13,13 @@ vi.mock('$lib/api', async (importOriginal) => ({ const mockSetUserSettings = vi.fn(); -vi.mock('$stores/userSettings.svelte', () => ({ +vi.mock('#stores/userSettings.svelte.ts', () => ({ setUserSettings: (settings: unknown) => mockSetUserSettings(settings), })); const mockSetTheme = vi.fn(); -vi.mock('$stores/theme.svelte', () => ({ +vi.mock('#stores/theme.svelte.ts', () => ({ setTheme: (theme: string) => mockSetTheme(theme), })); @@ -27,11 +27,11 @@ const mockAuthStore = { isAuthenticated: true as boolean | null, }; -vi.mock('$stores/auth.svelte', () => ({ +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mockAuthStore, })); -vi.mock('$lib/api-interceptors', () => ({})); +vi.mock('#lib/api-interceptors.ts', () => ({})); describe('user-settings', () => { beforeEach(async () => { @@ -60,7 +60,7 @@ describe('user-settings', () => { error: null, }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); await loadUserSettings(); expect(mockGetUserSettings).toHaveBeenCalledWith({}); @@ -70,7 +70,7 @@ describe('user-settings', () => { const apiSettings = { theme: 'system', editor: { tab_size: 4 } }; mockGetUserSettings.mockResolvedValue({ data: apiSettings, error: null }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); await loadUserSettings(); expect(mockSetUserSettings).toHaveBeenCalledWith(apiSettings); @@ -82,7 +82,7 @@ describe('user-settings', () => { error: null, }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); await loadUserSettings(); expect(mockSetTheme).toHaveBeenCalledWith('dark'); @@ -94,7 +94,7 @@ describe('user-settings', () => { error: { detail: 'Not found' }, }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); const result = await loadUserSettings(); expect(result).toBeUndefined(); @@ -103,7 +103,7 @@ describe('user-settings', () => { it('returns undefined on network error', async () => { mockGetUserSettings.mockResolvedValue({ data: undefined, error: new Error('Network error') }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); const result = await loadUserSettings(); expect(result).toBeUndefined(); @@ -115,7 +115,7 @@ describe('user-settings', () => { error: null, }); - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); await loadUserSettings(); expect(mockSetTheme).toHaveBeenCalledWith('auto'); @@ -127,7 +127,7 @@ describe('user-settings', () => { mockAuthStore.isAuthenticated = false; vi.resetModules(); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); const result = await saveUserSettings({ theme: 'dark' }); expect(result).toBe(false); @@ -137,7 +137,7 @@ describe('user-settings', () => { it('calls API with partial settings', async () => { mockUpdateUserSettings.mockResolvedValue({ data: {}, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); await saveUserSettings({ theme: 'dark' }); expect(mockUpdateUserSettings).toHaveBeenCalledWith({ @@ -149,7 +149,7 @@ describe('user-settings', () => { mockUpdateUserSettings.mockResolvedValue({ data: {}, error: null }); const editorSettings = { font_size: 16, tab_size: 2 }; - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); await saveUserSettings({ editor: editorSettings }); expect(mockUpdateUserSettings).toHaveBeenCalledWith({ @@ -160,7 +160,7 @@ describe('user-settings', () => { it('can save multiple settings at once', async () => { mockUpdateUserSettings.mockResolvedValue({ data: {}, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); await saveUserSettings({ theme: 'dark', editor: { font_size: 18 } }); expect(mockUpdateUserSettings).toHaveBeenCalledWith({ @@ -172,8 +172,8 @@ describe('user-settings', () => { const responseData = { user_id: '123', theme: 'system' }; mockUpdateUserSettings.mockResolvedValue({ data: responseData, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); - await saveUserSettings({ theme: 'system' as import('$lib/api').Theme }); + const { saveUserSettings } = await import('#lib/user-settings.ts'); + await saveUserSettings({ theme: 'system' as import('#lib/api/index.ts').Theme }); expect(mockSetUserSettings).toHaveBeenCalledWith(responseData); }); @@ -182,7 +182,7 @@ describe('user-settings', () => { const responseData = { user_id: '123', theme: 'dark' }; mockUpdateUserSettings.mockResolvedValue({ data: responseData, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); await saveUserSettings({ theme: 'dark' }); expect(mockSetTheme).toHaveBeenCalledWith('dark'); @@ -192,7 +192,7 @@ describe('user-settings', () => { const responseData = { user_id: '123', theme: 'auto', editor: { font_size: 16 } }; mockUpdateUserSettings.mockResolvedValue({ data: responseData, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); await saveUserSettings({ editor: { font_size: 16 } }); expect(mockSetTheme).toHaveBeenCalledWith('auto'); @@ -201,7 +201,7 @@ describe('user-settings', () => { it('returns true on success', async () => { mockUpdateUserSettings.mockResolvedValue({ data: {}, error: null }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); const result = await saveUserSettings({ theme: 'light' }); expect(result).toBe(true); @@ -213,7 +213,7 @@ describe('user-settings', () => { error: { detail: 'Server error' }, }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); const result = await saveUserSettings({ theme: 'dark' }); expect(result).toBe(false); @@ -222,7 +222,7 @@ describe('user-settings', () => { it('returns false on network error', async () => { mockUpdateUserSettings.mockResolvedValue({ data: undefined, error: new Error('Network error') }); - const { saveUserSettings } = await import('$lib/user-settings'); + const { saveUserSettings } = await import('#lib/user-settings.ts'); const result = await saveUserSettings({ theme: 'dark' }); expect(result).toBe(false); diff --git a/frontend/src/lib/__tests__/validation.test.ts b/frontend/src/lib/__tests__/validation.test.ts index bbe352faa..4eb4b8ed0 100644 --- a/frontend/src/lib/__tests__/validation.test.ts +++ b/frontend/src/lib/__tests__/validation.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { zSystemSettingsSchemaInput, zUserCreate } from '$lib/api'; -import { firstIssue } from '$lib/validation'; +import { zSystemSettingsSchemaInput, zUserCreate } from '#lib/api/index.ts'; +import { firstIssue } from '#lib/validation.ts'; function issueFor(result: { success: boolean; error?: unknown }): string { if (result.success) throw new Error('expected the parse to fail'); @@ -32,7 +32,7 @@ describe('firstIssue', () => { [ 'a missing required field', zUserCreate.safeParse({ email: 'u@example.com', password: 'password123' }), - 'Username: Invalid input: expected string, received undefined', + 'Username must be a string', ], ])('phrases %s from the generated schema', (_name, result, expected) => { expect(issueFor(result)).toBe(expected); diff --git a/frontend/src/lib/admin/__tests__/constants.test.ts b/frontend/src/lib/admin/__tests__/constants.test.ts index 9c26b47ba..6dd2eadeb 100644 --- a/frontend/src/lib/admin/__tests__/constants.test.ts +++ b/frontend/src/lib/admin/__tests__/constants.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { STATS_BG_COLORS, STATS_TEXT_COLORS } from '$lib/admin/constants'; +import { STATS_BG_COLORS, STATS_TEXT_COLORS } from '#lib/admin/constants.ts'; describe('admin constants', () => { describe('STATS_BG_COLORS', () => { diff --git a/frontend/src/lib/admin/__tests__/pagination.test.ts b/frontend/src/lib/admin/__tests__/pagination.test.ts index 573a9fd62..7c3dd73bd 100644 --- a/frontend/src/lib/admin/__tests__/pagination.test.ts +++ b/frontend/src/lib/admin/__tests__/pagination.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; -const { createPaginationState } = await import('$lib/admin/pagination.svelte'); +const { createPaginationState } = await import('#lib/admin/pagination.svelte.ts'); describe('createPaginationState', () => { describe('defaults', () => { diff --git a/frontend/src/lib/admin/events/__tests__/eventTypes.test.ts b/frontend/src/lib/admin/events/__tests__/eventTypes.test.ts index 5c5619953..75167b832 100644 --- a/frontend/src/lib/admin/events/__tests__/eventTypes.test.ts +++ b/frontend/src/lib/admin/events/__tests__/eventTypes.test.ts @@ -6,8 +6,8 @@ import { hasActiveFilters, getActiveFilterCount, getActiveFilterSummary, -} from '$lib/admin/events/eventTypes'; -import type { EventFilter } from '$lib/api'; +} from '#lib/admin/events/eventTypes.ts'; +import type { EventFilter } from '#lib/api/index.ts'; const withFilter = (override: Partial): EventFilter => ({ ...override }); @@ -45,7 +45,7 @@ describe('eventTypes', () => { ['pod_terminated', 'text-orange'], ['unknown_event', 'text-neutral'], ])('%s returns %s', (eventType, expectedColor) => { - const color = getEventTypeColor(eventType as import('$lib/api').EventType); + const color = getEventTypeColor(eventType as import('#lib/api/index.ts').EventType); expect(color).toContain(expectedColor); expect(color).toContain('dark:'); }); @@ -59,7 +59,7 @@ describe('eventTypes', () => { ['single', 'single'], ['a_b_c', 'a_b_c'], ])('%s returns %s', (input, expected) => { - expect(getEventTypeLabel(input as import('$lib/api').EventType)).toBe(expected); + expect(getEventTypeLabel(input as import('#lib/api/index.ts').EventType)).toBe(expected); }); }); @@ -84,10 +84,10 @@ describe('eventTypes', () => { describe('getActiveFilterCount', () => { it.each([ [{}, 0], - [withFilter({ event_types: ['x' as import('$lib/api').EventType], search_text: 'y' }), 2], + [withFilter({ event_types: ['x' as import('#lib/api/index.ts').EventType], search_text: 'y' }), 2], [ withFilter({ - event_types: ['x' as import('$lib/api').EventType], + event_types: ['x' as import('#lib/api/index.ts').EventType], search_text: 'y', aggregate_id: 'a', user_id: 'u', @@ -120,7 +120,7 @@ describe('eventTypes', () => { it('includes all active filter labels', () => { const summary = getActiveFilterSummary( withFilter({ - event_types: ['x' as import('$lib/api').EventType], + event_types: ['x' as import('#lib/api/index.ts').EventType], search_text: 'y', aggregate_id: 'a', user_id: 'u', diff --git a/frontend/src/lib/admin/events/eventTypes.ts b/frontend/src/lib/admin/events/eventTypes.ts index 1dbf2ef08..86fe3aba4 100644 --- a/frontend/src/lib/admin/events/eventTypes.ts +++ b/frontend/src/lib/admin/events/eventTypes.ts @@ -1,10 +1,10 @@ /** * Event type configurations and utilities */ -import { EventType, type EventFilter } from '$lib/api'; +import { EventType, type EventFilter } from '#lib/api/index.ts'; // Re-export EventType from generated types -export type { EventType } from '$lib/api'; +export type { EventType } from '#lib/api/index.ts'; // Every event type, for the filter UI export const EVENT_TYPES: EventType[] = Object.values(EventType); diff --git a/frontend/src/lib/admin/events/index.ts b/frontend/src/lib/admin/events/index.ts index 86c136bd0..3c1de8a72 100644 --- a/frontend/src/lib/admin/events/index.ts +++ b/frontend/src/lib/admin/events/index.ts @@ -1 +1 @@ -export * from '$lib/admin/events/eventTypes'; +export * from '#lib/admin/events/eventTypes.ts'; diff --git a/frontend/src/lib/admin/index.ts b/frontend/src/lib/admin/index.ts index 285688564..8bb791659 100644 --- a/frontend/src/lib/admin/index.ts +++ b/frontend/src/lib/admin/index.ts @@ -1,8 +1,8 @@ // Shared admin utilities -export * from '$lib/admin/pagination.svelte'; -export * from '$lib/admin/constants'; +export * from '#lib/admin/pagination.svelte.ts'; +export * from '#lib/admin/constants.ts'; // Domain-specific exports -export * from '$lib/admin/sagas'; -export * from '$lib/admin/events'; -export * from '$lib/admin/rate-limits'; +export * from '#lib/admin/sagas/index.ts'; +export * from '#lib/admin/events/index.ts'; +export * from '#lib/admin/rate-limits/index.ts'; diff --git a/frontend/src/lib/admin/rate-limits/__tests__/rateLimits.test.ts b/frontend/src/lib/admin/rate-limits/__tests__/rateLimits.test.ts index 5fb0a3b63..347ba8cb8 100644 --- a/frontend/src/lib/admin/rate-limits/__tests__/rateLimits.test.ts +++ b/frontend/src/lib/admin/rate-limits/__tests__/rateLimits.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import type { EndpointGroup } from '$lib/api'; -import { getGroupColor, detectGroupFromEndpoint, createEmptyRule } from '$lib/admin/rate-limits/rateLimits'; +import type { EndpointGroup } from '#lib/api/index.ts'; +import { getGroupColor, detectGroupFromEndpoint, createEmptyRule } from '#lib/admin/rate-limits/rateLimits.ts'; const EXPECTED_GROUPS: EndpointGroup[] = ['execution', 'admin', 'sse', 'websocket', 'auth', 'api', 'public']; diff --git a/frontend/src/lib/admin/rate-limits/index.ts b/frontend/src/lib/admin/rate-limits/index.ts index 3d7bc819c..299d83a73 100644 --- a/frontend/src/lib/admin/rate-limits/index.ts +++ b/frontend/src/lib/admin/rate-limits/index.ts @@ -1 +1 @@ -export * from '$lib/admin/rate-limits/rateLimits'; +export * from '#lib/admin/rate-limits/rateLimits.ts'; diff --git a/frontend/src/lib/admin/rate-limits/rateLimits.ts b/frontend/src/lib/admin/rate-limits/rateLimits.ts index 28b886270..6f42518a1 100644 --- a/frontend/src/lib/admin/rate-limits/rateLimits.ts +++ b/frontend/src/lib/admin/rate-limits/rateLimits.ts @@ -1,7 +1,7 @@ /** * Rate limit configurations and utilities for user management */ -import { EndpointGroup, zRateLimitRuleRequest, type RateLimitRuleResponse } from '$lib/api'; +import { EndpointGroup, zRateLimitRuleRequest, type RateLimitRuleResponse } from '#lib/api/index.ts'; // Group colors for rate limit endpoint groups const GROUP_COLORS: Record = { diff --git a/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts b/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts index fcd2aee4d..420c5728e 100644 --- a/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts +++ b/frontend/src/lib/admin/sagas/__tests__/sagaStates.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { SAGA_STATES, getSagaStateInfo } from '$lib/admin/sagas/sagaStates'; +import { SAGA_STATES, getSagaStateInfo } from '#lib/admin/sagas/sagaStates.ts'; describe('sagaStates', () => { describe('SAGA_STATES', () => { diff --git a/frontend/src/lib/admin/sagas/index.ts b/frontend/src/lib/admin/sagas/index.ts index d4dbc36ca..ca298e1a7 100644 --- a/frontend/src/lib/admin/sagas/index.ts +++ b/frontend/src/lib/admin/sagas/index.ts @@ -1 +1 @@ -export * from '$lib/admin/sagas/sagaStates'; +export * from '#lib/admin/sagas/sagaStates.ts'; diff --git a/frontend/src/lib/admin/sagas/sagaStates.ts b/frontend/src/lib/admin/sagas/sagaStates.ts index e930d35f1..2775869f0 100644 --- a/frontend/src/lib/admin/sagas/sagaStates.ts +++ b/frontend/src/lib/admin/sagas/sagaStates.ts @@ -2,7 +2,7 @@ * Saga state configurations */ import { Plus, Loader, TriangleAlert, CircleCheckBig, CircleX, Clock } from '@lucide/svelte'; -import type { SagaState } from '$lib/api'; +import type { SagaState } from '#lib/api/index.ts'; export interface SagaStateConfig { label: string; diff --git a/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts b/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts index 1355eee77..1bbcb38c3 100644 --- a/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts +++ b/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { effect_root } from 'svelte/internal/client'; -import { createMockEvent, createMockStats, mockApi } from '$test/test-utils'; +import { createMockEvent, createMockStats, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; import { browseEventsApiV1AdminEventsBrowsePost, @@ -11,8 +11,8 @@ import { getUserOverviewApiV1AdminUsersUserIdOverviewGet, streamReplayStatusApiV1AdminEventsReplaySessionIdStatusGet, type EventReplayStatusResponse, -} from '$lib/api'; -import { createEventsStore } from '$lib/admin/stores/eventsStore.svelte'; +} from '#lib/api/index.ts'; +import { createEventsStore } from '#lib/admin/stores/eventsStore.svelte.ts'; const windowOpen = vi.fn(); const windowConfirm = vi.fn(); diff --git a/frontend/src/lib/admin/stores/__tests__/executionsStore.test.ts b/frontend/src/lib/admin/stores/__tests__/executionsStore.test.ts index df5ab047f..4fde890a8 100644 --- a/frontend/src/lib/admin/stores/__tests__/executionsStore.test.ts +++ b/frontend/src/lib/admin/stores/__tests__/executionsStore.test.ts @@ -1,13 +1,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { effect_root } from 'svelte/internal/client'; -import { createMockExecution, createMockQueueStatus, mockApi } from '$test/test-utils'; +import { createMockExecution, createMockQueueStatus, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; import { listExecutionsApiV1AdminExecutionsGet, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, getQueueStatusApiV1AdminExecutionsQueueGet, -} from '$lib/api'; -import { createExecutionsStore } from '$lib/admin/stores/executionsStore.svelte'; +} from '#lib/api/index.ts'; +import { createExecutionsStore } from '#lib/admin/stores/executionsStore.svelte.ts'; function setupDefaultMocks() { mockApi(listExecutionsApiV1AdminExecutionsGet).ok({ diff --git a/frontend/src/lib/admin/stores/__tests__/sagasStore.test.ts b/frontend/src/lib/admin/stores/__tests__/sagasStore.test.ts index 3aad31bc0..f4d51ff66 100644 --- a/frontend/src/lib/admin/stores/__tests__/sagasStore.test.ts +++ b/frontend/src/lib/admin/stores/__tests__/sagasStore.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { effect_root } from 'svelte/internal/client'; -import { createMockSaga, mockApi } from '$test/test-utils'; -import { listSagasApiV1SagasGet } from '$lib/api'; -import { createSagasStore } from '$lib/admin/stores/sagasStore.svelte'; +import { createMockSaga, mockApi } from '#test/test-utils.ts'; +import { listSagasApiV1SagasGet } from '#lib/api/index.ts'; +import { createSagasStore } from '#lib/admin/stores/sagasStore.svelte.ts'; describe('SagasStore', () => { let store: ReturnType; diff --git a/frontend/src/lib/admin/stores/eventsStore.svelte.ts b/frontend/src/lib/admin/stores/eventsStore.svelte.ts index b16018f2b..caac7007d 100644 --- a/frontend/src/lib/admin/stores/eventsStore.svelte.ts +++ b/frontend/src/lib/admin/stores/eventsStore.svelte.ts @@ -16,13 +16,13 @@ import { type EventReplayResponse, type EventReplayStatusResponse, type AdminUserOverview, -} from '$lib/api'; -import { unwrap, unwrapOr } from '$lib/api-interceptors'; +} from '#lib/api/index.ts'; +import { unwrap, unwrapOr } from '#lib/api-interceptors.ts'; import { toast } from 'svelte-sonner'; -import { createPaginationState } from '$lib/admin/pagination.svelte'; -import { pollWhileVisible } from '$lib/admin/pollWhileVisible.svelte'; +import { createPaginationState } from '#lib/admin/pagination.svelte.ts'; +import { pollWhileVisible } from '#lib/admin/pollWhileVisible.svelte.ts'; import { parseISO } from 'date-fns'; -import { logger } from '$lib/logger'; +import { logger } from '#lib/logger.ts'; const log = logger.withTag('EventsStore'); const POLL_INTERVAL_MS = 30_000; diff --git a/frontend/src/lib/admin/stores/executionsStore.svelte.ts b/frontend/src/lib/admin/stores/executionsStore.svelte.ts index 5d5466275..a0747613a 100644 --- a/frontend/src/lib/admin/stores/executionsStore.svelte.ts +++ b/frontend/src/lib/admin/stores/executionsStore.svelte.ts @@ -6,11 +6,11 @@ import { type QueueStatusResponse, type QueuePriority, type ExecutionStatus, -} from '$lib/api'; -import { unwrap, unwrapOr } from '$lib/api-interceptors'; +} from '#lib/api/index.ts'; +import { unwrap, unwrapOr } from '#lib/api-interceptors.ts'; import { toast } from 'svelte-sonner'; -import { createPaginationState } from '$lib/admin/pagination.svelte'; -import { pollWhileVisible } from '$lib/admin/pollWhileVisible.svelte'; +import { createPaginationState } from '#lib/admin/pagination.svelte.ts'; +import { pollWhileVisible } from '#lib/admin/pollWhileVisible.svelte.ts'; const POLL_INTERVAL_MS = 5_000; diff --git a/frontend/src/lib/admin/stores/sagasStore.svelte.ts b/frontend/src/lib/admin/stores/sagasStore.svelte.ts index d4b9657d9..a77c9ebbd 100644 --- a/frontend/src/lib/admin/stores/sagasStore.svelte.ts +++ b/frontend/src/lib/admin/stores/sagasStore.svelte.ts @@ -1,8 +1,8 @@ -import { listSagasApiV1SagasGet, type SagaStatusResponse } from '$lib/api'; -import { unwrapOr } from '$lib/api-interceptors'; -import { createPaginationState } from '$lib/admin/pagination.svelte'; -import { pollWhileVisible } from '$lib/admin/pollWhileVisible.svelte'; -import { type SagaStateFilter } from '$lib/admin/sagas'; +import { listSagasApiV1SagasGet, type SagaStatusResponse } from '#lib/api/index.ts'; +import { unwrapOr } from '#lib/api-interceptors.ts'; +import { createPaginationState } from '#lib/admin/pagination.svelte.ts'; +import { pollWhileVisible } from '#lib/admin/pollWhileVisible.svelte.ts'; +import { type SagaStateFilter } from '#lib/admin/sagas/index.ts'; class SagasStore { sagas = $state([]); diff --git a/frontend/src/lib/api-interceptors.ts b/frontend/src/lib/api-interceptors.ts index 9c4ba8274..f6fbcd75f 100644 --- a/frontend/src/lib/api-interceptors.ts +++ b/frontend/src/lib/api-interceptors.ts @@ -1,8 +1,8 @@ -import { client, type ValidationError } from '$lib/api'; +import { client, type ValidationError } from '#lib/api/index.ts'; import { toast } from 'svelte-sonner'; -import { goto } from '@mateothegreat/svelte5-router'; -import { authStore } from '$stores/auth.svelte'; -import { logger } from '$lib/logger'; +import { goto } from '$app/navigation'; +import { authStore } from '#stores/auth.svelte.ts'; +import { logger } from '#lib/logger.ts'; const log = logger.withTag('API'); @@ -62,7 +62,7 @@ function handle401(isAuthEndpoint: boolean): void { if (currentPath !== '/login' && currentPath !== '/register') { sessionStorage.setItem('redirectAfterLogin', currentPath); } - goto('/login'); + void goto('/login'); setTimeout(() => { isHandling401 = false; }, 1000); diff --git a/frontend/src/lib/api/zod.gen.ts b/frontend/src/lib/api/zod.gen.ts index 5d81976ac..8a6c167d1 100644 --- a/frontend/src/lib/api/zod.gen.ts +++ b/frontend/src/lib/api/zod.gen.ts @@ -1,17 +1,17 @@ // This file is auto-generated by @hey-api/openapi-ts -import * as z from 'zod'; +import * as z from 'zod/mini'; /** * Body_login_api_v1_auth_login_post */ export const zBodyLoginApiV1AuthLoginPost = z.object({ - grant_type: z.string().regex(/^password$/).nullish(), + grant_type: z.nullish(z.string().check(z.regex(/^password$/))), username: z.string(), password: z.string(), - scope: z.string().optional().default(''), - client_id: z.string().nullish(), - client_secret: z.string().nullish() + scope: z._default(z.optional(z.string()), ''), + client_id: z.nullish(z.string()), + client_secret: z.nullish(z.string()) }); /** @@ -20,7 +20,7 @@ export const zBodyLoginApiV1AuthLoginPost = z.object({ * Model for cancelling an execution. */ export const zCancelExecutionRequest = z.object({ - reason: z.string().optional().default('User requested cancellation') + reason: z._default(z.optional(z.string()), 'User requested cancellation') }); /** @@ -39,7 +39,7 @@ export const zCancelResponse = z.object({ execution_id: z.string(), status: zCancelStatus, message: z.string(), - event_id: z.string().nullable() + event_id: z.nullable(z.string()) }); /** @@ -59,9 +59,9 @@ export const zCleanupResponse = z.object({ */ export const zContainerStatusInfo = z.object({ name: z.string(), - ready: z.boolean().optional().default(false), - restart_count: z.int().optional().default(0), - state: z.string().optional().default('unknown') + ready: z._default(z.optional(z.boolean()), false), + restart_count: z._default(z.optional(z.int()), 0), + state: z._default(z.optional(z.string()), 'unknown') }); /** @@ -90,23 +90,23 @@ export const zDeleteResponse = z.object({ */ export const zDeleteUserResponse = z.object({ user_deleted: z.boolean(), - executions: z.int().default(0), - saved_scripts: z.int().default(0), - notifications: z.int().default(0), - user_settings: z.int().default(0), - events: z.int().default(0), - sagas: z.int().default(0) + executions: z._default(z.int(), 0), + saved_scripts: z._default(z.int(), 0), + notifications: z._default(z.int(), 0), + user_settings: z._default(z.int(), 0), + events: z._default(z.int(), 0), + sagas: z._default(z.int(), 0) }); /** * DerivedCounts */ export const zDerivedCounts = z.object({ - succeeded: z.int().default(0), - failed: z.int().default(0), - timeout: z.int().default(0), - cancelled: z.int().default(0), - terminal_total: z.int().default(0) + succeeded: z._default(z.int(), 0), + failed: z._default(z.int(), 0), + timeout: z._default(z.int(), 0), + cancelled: z._default(z.int(), 0), + terminal_total: z._default(z.int(), 0) }); /** @@ -115,11 +115,11 @@ export const zDerivedCounts = z.object({ * Code editor preferences */ export const zEditorSettingsInput = z.object({ - font_size: z.int().optional().default(14), - tab_size: z.int().optional().default(4), - use_tabs: z.boolean().optional().default(false), - word_wrap: z.boolean().optional().default(true), - show_line_numbers: z.boolean().optional().default(true) + font_size: z._default(z.optional(z.int()), 14), + tab_size: z._default(z.optional(z.int()), 4), + use_tabs: z._default(z.optional(z.boolean()), false), + word_wrap: z._default(z.optional(z.boolean()), true), + show_line_numbers: z._default(z.optional(z.boolean()), true) }); /** @@ -128,11 +128,11 @@ export const zEditorSettingsInput = z.object({ * Code editor preferences */ export const zEditorSettingsOutput = z.object({ - font_size: z.int().default(14), - tab_size: z.int().default(4), - use_tabs: z.boolean().default(false), - word_wrap: z.boolean().default(true), - show_line_numbers: z.boolean().default(true) + font_size: z._default(z.int(), 14), + tab_size: z._default(z.int(), 4), + use_tabs: z._default(z.boolean(), false), + word_wrap: z._default(z.boolean(), true), + show_line_numbers: z._default(z.boolean(), true) }); /** @@ -165,7 +165,7 @@ export const zEnvironment = z.enum([ */ export const zErrorResponse = z.object({ detail: z.string(), - type: z.string().nullable() + type: z.nullable(z.string()) }); /** @@ -187,7 +187,7 @@ export const zEventMetadata = z.object({ service_name: z.string(), service_version: z.string(), user_id: z.string(), - environment: zEnvironment.optional().default('production') + environment: z._default(z.optional(zEnvironment), 'production') }); /** @@ -195,14 +195,14 @@ export const zEventMetadata = z.object({ */ export const zAllocateResourcesCommandEvent = z.object({ event_id: z.string(), - event_type: z.literal('allocate_resources_command').default('allocate_resources_command'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('allocate_resources_command'), 'allocate_resources_command'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - cpu_request: z.string().optional(), - memory_request: z.string().optional() + execution_id: z.optional(z.string()), + cpu_request: z.optional(z.string()), + memory_request: z.optional(z.string()) }); /** @@ -210,14 +210,14 @@ export const zAllocateResourcesCommandEvent = z.object({ */ export const zAuthFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('auth_failed').default('auth_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('auth_failed'), 'auth_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - username: z.string().nullish(), - reason: z.string().optional(), - ip_address: z.string().nullish() + username: z.nullish(z.string()), + reason: z.optional(z.string()), + ip_address: z.nullish(z.string()) }); /** @@ -225,16 +225,16 @@ export const zAuthFailedEvent = z.object({ */ export const zDeletePodCommandEvent = z.object({ event_id: z.string(), - event_type: z.literal('delete_pod_command').default('delete_pod_command'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('delete_pod_command'), 'delete_pod_command'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - execution_id: z.string().optional(), - reason: z.string().optional(), - pod_name: z.string().nullish(), - namespace: z.string().nullish() + saga_id: z.optional(z.string()), + execution_id: z.optional(z.string()), + reason: z.optional(z.string()), + pod_name: z.nullish(z.string()), + namespace: z.nullish(z.string()) }); /** @@ -243,12 +243,12 @@ export const zDeletePodCommandEvent = z.object({ * Request model for replaying events */ export const zEventReplayRequest = z.object({ - event_ids: z.array(z.string()).nullish(), - aggregate_id: z.string().nullish(), - start_time: z.iso.datetime().nullish(), - end_time: z.iso.datetime().nullish(), - target_service: z.string().nullish(), - dry_run: z.boolean().optional().default(true) + event_ids: z.nullish(z.array(z.string())), + aggregate_id: z.nullish(z.string()), + start_time: z.nullish(z.iso.datetime()), + end_time: z.nullish(z.iso.datetime()), + target_service: z.nullish(z.string()), + dry_run: z._default(z.optional(z.boolean()), true) }); /** @@ -323,16 +323,16 @@ export const zEventType = z.enum([ */ export const zDlqMessageDiscardedEvent = z.object({ event_id: z.string(), - event_type: z.literal('dlq_message_discarded').default('dlq_message_discarded'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('dlq_message_discarded'), 'dlq_message_discarded'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - dlq_event_id: z.string().optional(), - original_topic: z.string().optional(), - original_event_type: zEventType.optional(), - reason: z.string().optional(), - retry_count: z.int().optional() + dlq_event_id: z.optional(z.string()), + original_topic: z.optional(z.string()), + original_event_type: z.optional(zEventType), + reason: z.optional(z.string()), + retry_count: z.optional(z.int()) }); /** @@ -342,18 +342,18 @@ export const zDlqMessageDiscardedEvent = z.object({ */ export const zDlqMessageReceivedEvent = z.object({ event_id: z.string(), - event_type: z.literal('dlq_message_received').default('dlq_message_received'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('dlq_message_received'), 'dlq_message_received'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - dlq_event_id: z.string().optional(), - original_topic: z.string().optional(), - original_event_type: zEventType.optional(), - error: z.string().optional(), - retry_count: z.int().optional(), - producer_id: z.string().optional(), - failed_at: z.iso.datetime().optional() + dlq_event_id: z.optional(z.string()), + original_topic: z.optional(z.string()), + original_event_type: z.optional(zEventType), + error: z.optional(z.string()), + retry_count: z.optional(z.int()), + producer_id: z.optional(z.string()), + failed_at: z.optional(z.iso.datetime()) }); /** @@ -363,16 +363,16 @@ export const zDlqMessageReceivedEvent = z.object({ */ export const zDlqMessageRetriedEvent = z.object({ event_id: z.string(), - event_type: z.literal('dlq_message_retried').default('dlq_message_retried'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('dlq_message_retried'), 'dlq_message_retried'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - dlq_event_id: z.string().optional(), - original_topic: z.string().optional(), - original_event_type: zEventType.optional(), - retry_count: z.int().optional(), - retry_topic: z.string().optional() + dlq_event_id: z.optional(z.string()), + original_topic: z.optional(z.string()), + original_event_type: z.optional(zEventType), + retry_count: z.optional(z.int()), + retry_topic: z.optional(z.string()) }); /** @@ -381,13 +381,13 @@ export const zDlqMessageRetriedEvent = z.object({ * Filter criteria for browsing events */ export const zEventFilter = z.object({ - event_types: z.array(zEventType).nullish(), - aggregate_id: z.string().nullish(), - user_id: z.string().nullish(), - start_time: z.iso.datetime().nullish(), - end_time: z.iso.datetime().nullish(), - service_name: z.string().nullish(), - search_text: z.string().nullish() + event_types: z.nullish(z.array(zEventType)), + aggregate_id: z.nullish(z.string()), + user_id: z.nullish(z.string()), + start_time: z.nullish(z.iso.datetime()), + end_time: z.nullish(z.iso.datetime()), + service_name: z.nullish(z.string()), + search_text: z.nullish(z.string()) }); /** @@ -397,8 +397,8 @@ export const zEventFilter = z.object({ */ export const zEventBrowseRequest = z.object({ filters: zEventFilter, - skip: z.int().optional().default(0), - limit: z.int().lte(500).optional().default(50) + skip: z._default(z.optional(z.int()), 0), + limit: z._default(z.optional(z.int().check(z.lte(500))), 50) }); /** @@ -408,7 +408,7 @@ export const zEventSummary = z.object({ event_id: z.string(), event_type: zEventType, timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish() + aggregate_id: z.nullish(z.string()) }); /** @@ -433,14 +433,14 @@ export const zExampleScripts = z.object({ */ export const zExecutionCancelledEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_cancelled').default('execution_cancelled'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_cancelled'), 'execution_cancelled'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - cancelled_by: z.string().optional(), - force_terminated: z.boolean().optional().default(false) + execution_id: z.optional(z.string()), + cancelled_by: z.optional(z.string()), + force_terminated: z._default(z.optional(z.boolean()), false) }); /** @@ -461,14 +461,14 @@ export const zExecutionErrorType = z.enum([ */ export const zExecutionQueuedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_queued').default('execution_queued'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_queued'), 'execution_queued'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - position_in_queue: z.int().nullish(), - estimated_start_time: z.iso.datetime().nullish() + execution_id: z.optional(z.string()), + position_in_queue: z.nullish(z.int()), + estimated_start_time: z.nullish(z.iso.datetime()) }); /** @@ -477,9 +477,9 @@ export const zExecutionQueuedEvent = z.object({ * Model for execution request. */ export const zExecutionRequest = z.object({ - script: z.string().max(50000), - lang: z.string().optional().default('python'), - lang_version: z.string().optional().default('3.14') + script: z.string().check(z.maxLength(50000)), + lang: z._default(z.optional(z.string()), 'python'), + lang_version: z._default(z.optional(z.string()), '3.14') }); /** @@ -487,14 +487,14 @@ export const zExecutionRequest = z.object({ */ export const zExecutionRunningEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_running').default('execution_running'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_running'), 'execution_running'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - progress_percentage: z.int().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + progress_percentage: z.nullish(z.int()) }); /** @@ -502,15 +502,15 @@ export const zExecutionRunningEvent = z.object({ */ export const zExecutionStartedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_started').default('execution_started'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_started'), 'execution_started'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - node_name: z.string().nullish(), - container_id: z.string().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + node_name: z.nullish(z.string()), + container_id: z.nullish(z.string()) }); /** @@ -625,14 +625,14 @@ export const zMessageResponse = z.object({ */ export const zNotificationAllReadEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_all_read').default('notification_all_read'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_all_read'), 'notification_all_read'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - count: z.int().optional(), - read_at: z.iso.datetime().optional() + user_id: z.optional(z.string()), + count: z.optional(z.int()), + read_at: z.optional(z.iso.datetime()) }); /** @@ -651,15 +651,15 @@ export const zNotificationChannel = z.enum([ */ export const zNotificationClickedEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_clicked').default('notification_clicked'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_clicked'), 'notification_clicked'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - clicked_at: z.iso.datetime().optional(), - action: z.string().nullish() + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + clicked_at: z.optional(z.iso.datetime()), + action: z.nullish(z.string()) }); /** @@ -667,15 +667,15 @@ export const zNotificationClickedEvent = z.object({ */ export const zNotificationDeliveredEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_delivered').default('notification_delivered'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_delivered'), 'notification_delivered'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - channel: zNotificationChannel.optional(), - delivered_at: z.iso.datetime().optional() + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + channel: z.optional(zNotificationChannel), + delivered_at: z.optional(z.iso.datetime()) }); /** @@ -683,16 +683,16 @@ export const zNotificationDeliveredEvent = z.object({ */ export const zNotificationFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_failed').default('notification_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_failed'), 'notification_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - channel: zNotificationChannel.optional(), - error: z.string().optional(), - retry_count: z.int().optional().default(0) + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + channel: z.optional(zNotificationChannel), + error: z.optional(z.string()), + retry_count: z._default(z.optional(z.int()), 0) }); /** @@ -700,13 +700,13 @@ export const zNotificationFailedEvent = z.object({ */ export const zNotificationPreferencesUpdatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_preferences_updated').default('notification_preferences_updated'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_preferences_updated'), 'notification_preferences_updated'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - changed_fields: z.array(z.string()).optional() + user_id: z.optional(z.string()), + changed_fields: z.optional(z.array(z.string())) }); /** @@ -714,14 +714,14 @@ export const zNotificationPreferencesUpdatedEvent = z.object({ */ export const zNotificationReadEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_read').default('notification_read'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_read'), 'notification_read'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - read_at: z.iso.datetime().optional() + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + read_at: z.optional(z.iso.datetime()) }); /** @@ -729,15 +729,15 @@ export const zNotificationReadEvent = z.object({ */ export const zNotificationSentEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_sent').default('notification_sent'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_sent'), 'notification_sent'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - channel: zNotificationChannel.optional(), - sent_at: z.iso.datetime().optional() + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + channel: z.optional(zNotificationChannel), + sent_at: z.optional(z.iso.datetime()) }); /** @@ -746,11 +746,11 @@ export const zNotificationSentEvent = z.object({ * User notification preferences */ export const zNotificationSettingsInput = z.object({ - execution_completed: z.boolean().optional().default(true), - execution_failed: z.boolean().optional().default(true), - system_updates: z.boolean().optional().default(true), - security_alerts: z.boolean().optional().default(true), - channels: z.array(zNotificationChannel).optional().default(['in_app']) + execution_completed: z._default(z.optional(z.boolean()), true), + execution_failed: z._default(z.optional(z.boolean()), true), + system_updates: z._default(z.optional(z.boolean()), true), + security_alerts: z._default(z.optional(z.boolean()), true), + channels: z._default(z.optional(z.array(zNotificationChannel)), ['in_app']) }); /** @@ -759,11 +759,11 @@ export const zNotificationSettingsInput = z.object({ * User notification preferences */ export const zNotificationSettingsOutput = z.object({ - execution_completed: z.boolean().default(true), - execution_failed: z.boolean().default(true), - system_updates: z.boolean().default(true), - security_alerts: z.boolean().default(true), - channels: z.array(zNotificationChannel).default(['in_app']) + execution_completed: z._default(z.boolean(), true), + execution_failed: z._default(z.boolean(), true), + system_updates: z._default(z.boolean(), true), + security_alerts: z._default(z.boolean(), true), + channels: z._default(z.array(zNotificationChannel), ['in_app']) }); /** @@ -783,18 +783,18 @@ export const zNotificationSeverity = z.enum([ */ export const zNotificationCreatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('notification_created').default('notification_created'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('notification_created'), 'notification_created'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - notification_id: z.string().optional(), - user_id: z.string().optional(), - subject: z.string().optional(), - body: z.string().optional(), - severity: zNotificationSeverity.optional(), - tags: z.array(z.string()).optional(), - channels: z.array(zNotificationChannel).optional() + notification_id: z.optional(z.string()), + user_id: z.optional(z.string()), + subject: z.optional(z.string()), + body: z.optional(z.string()), + severity: z.optional(zNotificationSeverity), + tags: z.optional(z.array(z.string())), + channels: z.optional(z.array(zNotificationChannel)) }); /** @@ -826,7 +826,7 @@ export const zNotificationResponse = z.object({ body: z.string(), action_url: z.string(), created_at: z.iso.datetime(), - read_at: z.iso.datetime().nullable(), + read_at: z.nullable(z.iso.datetime()), severity: zNotificationSeverity, tags: z.array(z.string()) }); @@ -853,14 +853,14 @@ export const zNotificationSubscription = z.object({ severities: z.array(zNotificationSeverity), include_tags: z.array(z.string()), exclude_tags: z.array(z.string()), - enabled: z.boolean().default(true), - webhook_url: z.string().nullable(), - slack_webhook: z.string().nullable(), - quiet_hours_enabled: z.boolean().default(false), - quiet_hours_start: z.string().nullable(), - quiet_hours_end: z.string().nullable(), - timezone: z.string().default('UTC'), - batch_interval_minutes: z.int().default(60), + enabled: z._default(z.boolean(), true), + webhook_url: z.nullable(z.string()), + slack_webhook: z.nullable(z.string()), + quiet_hours_enabled: z._default(z.boolean(), false), + quiet_hours_start: z.nullable(z.string()), + quiet_hours_end: z.nullable(z.string()), + timezone: z._default(z.string(), 'UTC'), + batch_interval_minutes: z._default(z.int(), 60), created_at: z.iso.datetime(), updated_at: z.iso.datetime() }); @@ -871,7 +871,7 @@ export const zNotificationSubscription = z.object({ * Request model for password reset */ export const zPasswordResetRequest = z.object({ - new_password: z.string().min(8) + new_password: z.string().check(z.minLength(8)) }); /** @@ -879,14 +879,14 @@ export const zPasswordResetRequest = z.object({ */ export const zPodCreatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_created').default('pod_created'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_created'), 'pod_created'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - namespace: z.string().optional().default('default') + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + namespace: z._default(z.optional(z.string()), 'default') }); /** @@ -894,14 +894,14 @@ export const zPodCreatedEvent = z.object({ */ export const zPodDeletedEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_deleted').default('pod_deleted'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_deleted'), 'pod_deleted'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - reason: z.string().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + reason: z.nullish(z.string()) }); /** @@ -909,18 +909,18 @@ export const zPodDeletedEvent = z.object({ */ export const zPodFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_failed').default('pod_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_failed'), 'pod_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - exit_code: z.int().optional().default(1), - reason: z.string().nullish(), - message: z.string().nullish(), - stdout: z.string().nullish(), - stderr: z.string().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + exit_code: z._default(z.optional(z.int()), 1), + reason: z.nullish(z.string()), + message: z.nullish(z.string()), + stdout: z.nullish(z.string()), + stderr: z.nullish(z.string()) }); /** @@ -928,14 +928,14 @@ export const zPodFailedEvent = z.object({ */ export const zPodRunningEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_running').default('pod_running'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_running'), 'pod_running'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - container_statuses: z.array(zContainerStatusInfo).optional() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + container_statuses: z.optional(z.array(zContainerStatusInfo)) }); /** @@ -943,14 +943,14 @@ export const zPodRunningEvent = z.object({ */ export const zPodScheduledEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_scheduled').default('pod_scheduled'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_scheduled'), 'pod_scheduled'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - node_name: z.string().optional().default('') + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + node_name: z._default(z.optional(z.string()), '') }); /** @@ -958,16 +958,16 @@ export const zPodScheduledEvent = z.object({ */ export const zPodSucceededEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_succeeded').default('pod_succeeded'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_succeeded'), 'pod_succeeded'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - exit_code: z.int().optional().default(0), - stdout: z.string().nullish(), - stderr: z.string().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + exit_code: z._default(z.optional(z.int()), 0), + stdout: z.nullish(z.string()), + stderr: z.nullish(z.string()) }); /** @@ -975,16 +975,16 @@ export const zPodSucceededEvent = z.object({ */ export const zPodTerminatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('pod_terminated').default('pod_terminated'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('pod_terminated'), 'pod_terminated'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - pod_name: z.string().optional(), - exit_code: z.int().optional().default(0), - reason: z.string().nullish(), - message: z.string().nullish() + execution_id: z.optional(z.string()), + pod_name: z.optional(z.string()), + exit_code: z._default(z.optional(z.int()), 0), + reason: z.nullish(z.string()), + message: z.nullish(z.string()) }); /** @@ -1011,12 +1011,12 @@ export const zAdminExecutionResponse = z.object({ status: zExecutionStatus, lang: z.string(), lang_version: z.string(), - stdout: z.string().nullable(), - stderr: z.string().nullable(), - exit_code: z.int().nullable(), - error_type: zExecutionErrorType.nullable(), - priority: zQueuePriority.default('normal'), - user_id: z.string().nullable(), + stdout: z.nullable(z.string()), + stderr: z.nullable(z.string()), + exit_code: z.nullable(z.int()), + error_type: z.nullable(zExecutionErrorType), + priority: z._default(zQueuePriority, 'normal'), + user_id: z.nullable(z.string()), created_at: z.iso.datetime(), updated_at: z.iso.datetime() }); @@ -1039,25 +1039,25 @@ export const zAdminExecutionListResponse = z.object({ */ export const zCreatePodCommandEvent = z.object({ event_id: z.string(), - event_type: z.literal('create_pod_command').default('create_pod_command'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('create_pod_command'), 'create_pod_command'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - execution_id: z.string().optional(), - script: z.string().optional(), - language: z.string().optional(), - language_version: z.string().optional(), - runtime_image: z.string().optional(), - runtime_command: z.array(z.string()).optional(), - runtime_filename: z.string().optional(), - timeout_seconds: z.int().optional(), - cpu_limit: z.string().optional(), - memory_limit: z.string().optional(), - cpu_request: z.string().optional(), - memory_request: z.string().optional(), - priority: zQueuePriority.optional().default('normal') + saga_id: z.optional(z.string()), + execution_id: z.optional(z.string()), + script: z.optional(z.string()), + language: z.optional(z.string()), + language_version: z.optional(z.string()), + runtime_image: z.optional(z.string()), + runtime_command: z.optional(z.array(z.string())), + runtime_filename: z.optional(z.string()), + timeout_seconds: z.optional(z.int()), + cpu_limit: z.optional(z.string()), + memory_limit: z.optional(z.string()), + cpu_request: z.optional(z.string()), + memory_request: z.optional(z.string()), + priority: z._default(z.optional(zQueuePriority), 'normal') }); /** @@ -1065,15 +1065,15 @@ export const zCreatePodCommandEvent = z.object({ */ export const zExecutionAcceptedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_accepted').default('execution_accepted'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_accepted'), 'execution_accepted'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - queue_position: z.int().optional(), - estimated_wait_seconds: z.number().nullish(), - priority: zQueuePriority.optional().default('normal') + execution_id: z.optional(z.string()), + queue_position: z.optional(z.int()), + estimated_wait_seconds: z.nullish(z.number()), + priority: z._default(z.optional(zQueuePriority), 'normal') }); /** @@ -1081,24 +1081,24 @@ export const zExecutionAcceptedEvent = z.object({ */ export const zExecutionRequestedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_requested').default('execution_requested'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_requested'), 'execution_requested'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - script: z.string().optional(), - language: z.string().optional(), - language_version: z.string().optional(), - runtime_image: z.string().optional(), - runtime_command: z.array(z.string()).optional(), - runtime_filename: z.string().optional(), - timeout_seconds: z.int().optional(), - cpu_limit: z.string().optional(), - memory_limit: z.string().optional(), - cpu_request: z.string().optional(), - memory_request: z.string().optional(), - priority: zQueuePriority.optional().default('normal') + execution_id: z.optional(z.string()), + script: z.optional(z.string()), + language: z.optional(z.string()), + language_version: z.optional(z.string()), + runtime_image: z.optional(z.string()), + runtime_command: z.optional(z.array(z.string())), + runtime_filename: z.optional(z.string()), + timeout_seconds: z.optional(z.int()), + cpu_limit: z.optional(z.string()), + memory_limit: z.optional(z.string()), + cpu_request: z.optional(z.string()), + memory_request: z.optional(z.string()), + priority: z._default(z.optional(zQueuePriority), 'normal') }); /** @@ -1127,15 +1127,15 @@ export const zQueueStatusResponse = z.object({ */ export const zQuotaExceededEvent = z.object({ event_id: z.string(), - event_type: z.literal('quota_exceeded').default('quota_exceeded'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('quota_exceeded'), 'quota_exceeded'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - quota_type: z.string().optional(), - limit: z.int().optional(), - current_usage: z.int().optional(), - user_id: z.string().optional() + quota_type: z.optional(z.string()), + limit: z.optional(z.int()), + current_usage: z.optional(z.int()), + user_id: z.optional(z.string()) }); /** @@ -1163,15 +1163,15 @@ export const zEndpointUsageStats = z.object({ */ export const zRateLimitExceededEvent = z.object({ event_id: z.string(), - event_type: z.literal('rate_limit_exceeded').default('rate_limit_exceeded'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('rate_limit_exceeded'), 'rate_limit_exceeded'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().nullish(), - endpoint: z.string().optional(), - limit: z.int().optional(), - window_seconds: z.int().optional() + user_id: z.nullish(z.string()), + endpoint: z.optional(z.string()), + limit: z.optional(z.int()), + window_seconds: z.optional(z.int()) }); /** @@ -1184,10 +1184,10 @@ export const zRateLimitRuleRequest = z.object({ group: zEndpointGroup, requests: z.int(), window_seconds: z.int(), - algorithm: zRateLimitAlgorithm.optional().default('sliding_window'), - burst_multiplier: z.number().optional().default(1.5), - priority: z.int().optional().default(0), - enabled: z.boolean().optional().default(true) + algorithm: z._default(z.optional(zRateLimitAlgorithm), 'sliding_window'), + burst_multiplier: z._default(z.optional(z.number()), 1.5), + priority: z._default(z.optional(z.int()), 0), + enabled: z._default(z.optional(z.boolean()), true) }); /** @@ -1201,18 +1201,18 @@ export const zRateLimitRuleResponse = z.object({ requests: z.int(), window_seconds: z.int(), algorithm: zRateLimitAlgorithm, - burst_multiplier: z.number().default(1.5), - priority: z.int().default(0), - enabled: z.boolean().default(true) + burst_multiplier: z._default(z.number(), 1.5), + priority: z._default(z.int(), 0), + enabled: z._default(z.boolean(), true) }); /** * RateLimitSummary */ export const zRateLimitSummary = z.object({ - bypass_rate_limit: z.boolean().nullable(), - global_multiplier: z.number().nullable(), - has_custom_limits: z.boolean().nullable() + bypass_rate_limit: z.nullable(z.boolean()), + global_multiplier: z.nullable(z.number()), + has_custom_limits: z.nullable(z.boolean()) }); /** @@ -1221,10 +1221,10 @@ export const zRateLimitSummary = z.object({ * Request model for updating user rate limits. */ export const zRateLimitUpdateRequest = z.object({ - bypass_rate_limit: z.boolean().optional().default(false), - global_multiplier: z.number().optional().default(1), - rules: z.array(zRateLimitRuleRequest).optional().default([]), - notes: z.string().nullish() + bypass_rate_limit: z._default(z.optional(z.boolean()), false), + global_multiplier: z._default(z.optional(z.number()), 1), + rules: z._default(z.optional(z.array(zRateLimitRuleRequest)), []), + notes: z.nullish(z.string()) }); /** @@ -1232,14 +1232,14 @@ export const zRateLimitUpdateRequest = z.object({ */ export const zReleaseResourcesCommandEvent = z.object({ event_id: z.string(), - event_type: z.literal('release_resources_command').default('release_resources_command'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('release_resources_command'), 'release_resources_command'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - cpu_request: z.string().optional(), - memory_request: z.string().optional() + execution_id: z.optional(z.string()), + cpu_request: z.optional(z.string()), + memory_request: z.optional(z.string()) }); /** @@ -1258,38 +1258,38 @@ export const zReleaseResourcesCommandEvent = z.object({ export const zReplayError = z.object({ timestamp: z.iso.datetime(), error: z.string(), - error_type: z.string().nullable(), - event_id: z.string().nullable() + error_type: z.nullable(z.string()), + event_id: z.nullable(z.string()) }); /** * ReplayFilter */ export const zReplayFilter = z.object({ - event_ids: z.array(z.string()).nullish(), - execution_id: z.string().nullish(), - aggregate_id: z.string().nullish(), - event_types: z.array(zEventType).nullish(), - exclude_event_types: z.array(zEventType).nullish(), - start_time: z.iso.datetime().nullish(), - end_time: z.iso.datetime().nullish(), - user_id: z.string().nullish(), - service_name: z.string().nullish() + event_ids: z.nullish(z.array(z.string())), + execution_id: z.nullish(z.string()), + aggregate_id: z.nullish(z.string()), + event_types: z.nullish(z.array(zEventType)), + exclude_event_types: z.nullish(z.array(zEventType)), + start_time: z.nullish(z.iso.datetime()), + end_time: z.nullish(z.iso.datetime()), + user_id: z.nullish(z.string()), + service_name: z.nullish(z.string()) }); /** * ReplayFilterSchema */ export const zReplayFilterSchema = z.object({ - event_ids: z.array(z.string()).nullable(), - execution_id: z.string().nullable(), - aggregate_id: z.string().nullable(), - event_types: z.array(zEventType).nullable(), - exclude_event_types: z.array(zEventType).nullable(), - start_time: z.iso.datetime().nullable(), - end_time: z.iso.datetime().nullable(), - user_id: z.string().nullable(), - service_name: z.string().nullable() + event_ids: z.nullable(z.array(z.string())), + execution_id: z.nullable(z.string()), + aggregate_id: z.nullable(z.string()), + event_types: z.nullable(z.array(zEventType)), + exclude_event_types: z.nullable(z.array(zEventType)), + start_time: z.nullable(z.iso.datetime()), + end_time: z.nullable(z.iso.datetime()), + user_id: z.nullable(z.string()), + service_name: z.nullable(z.string()) }); /** @@ -1315,9 +1315,9 @@ export const zEventReplayResponse = z.object({ dry_run: z.boolean(), total_events: z.int(), replay_id: z.string(), - session_id: z.string().nullable(), + session_id: z.nullable(z.string()), status: zReplayStatus, - events_preview: z.array(zEventSummary).nullable() + events_preview: z.nullable(z.array(zEventSummary)) }); /** @@ -1357,18 +1357,18 @@ export const zReplayType = z.enum([ */ export const zReplayConfigSchema = z.object({ replay_type: zReplayType, - target: zReplayTarget.default('kafka'), + target: z._default(zReplayTarget, 'kafka'), filter: zReplayFilterSchema, - speed_multiplier: z.number().gte(0.1).lte(100).default(1), - preserve_timestamps: z.boolean().default(false), - batch_size: z.int().gte(1).lte(1000).default(100), - max_events: z.int().gte(1).nullable(), - target_topics: z.record(z.string(), z.string()).nullable(), - target_file_path: z.string().nullable(), - skip_errors: z.boolean().default(true), - retry_failed: z.boolean().default(false), - retry_attempts: z.int().default(3), - enable_progress_tracking: z.boolean().default(true) + speed_multiplier: z._default(z.number().check(z.gte(0.1), z.lte(100)), 1), + preserve_timestamps: z._default(z.boolean(), false), + batch_size: z._default(z.int().check(z.gte(1), z.lte(1000)), 100), + max_events: z.nullable(z.int().check(z.gte(1))), + target_topics: z.nullable(z.record(z.string(), z.string())), + target_file_path: z.nullable(z.string()), + skip_errors: z._default(z.boolean(), true), + retry_failed: z._default(z.boolean(), false), + retry_attempts: z._default(z.int(), 3), + enable_progress_tracking: z._default(z.boolean(), true) }); /** @@ -1378,18 +1378,18 @@ export const zReplayConfigSchema = z.object({ */ export const zReplayRequest = z.object({ replay_type: zReplayType, - target: zReplayTarget.optional().default('kafka'), - filter: zReplayFilter.optional(), - speed_multiplier: z.number().gte(0.1).lte(100).optional().default(1), - preserve_timestamps: z.boolean().optional().default(false), - batch_size: z.int().gte(1).lte(1000).optional().default(100), - max_events: z.int().gte(1).nullish(), - skip_errors: z.boolean().optional().default(true), - target_file_path: z.string().nullish(), - target_topics: z.record(z.string(), z.string()).nullish(), - retry_failed: z.boolean().optional().default(false), - retry_attempts: z.int().gte(1).lte(10).optional().default(3), - enable_progress_tracking: z.boolean().optional().default(true) + target: z._default(z.optional(zReplayTarget), 'kafka'), + filter: z.optional(zReplayFilter), + speed_multiplier: z._default(z.optional(z.number().check(z.gte(0.1), z.lte(100))), 1), + preserve_timestamps: z._default(z.optional(z.boolean()), false), + batch_size: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), + max_events: z.nullish(z.int().check(z.gte(1))), + skip_errors: z._default(z.optional(z.boolean()), true), + target_file_path: z.nullish(z.string()), + target_topics: z.nullish(z.record(z.string(), z.string())), + retry_failed: z._default(z.optional(z.boolean()), false), + retry_attempts: z._default(z.optional(z.int().check(z.gte(1), z.lte(10))), 3), + enable_progress_tracking: z._default(z.optional(z.boolean()), true) }); /** @@ -1398,15 +1398,15 @@ export const zReplayRequest = z.object({ export const zReplaySession = z.object({ session_id: z.string(), config: zReplayConfigSchema, - status: zReplayStatus.default('created'), - total_events: z.int().default(0), - replayed_events: z.int().default(0), - failed_events: z.int().default(0), - skipped_events: z.int().default(0), + status: z._default(zReplayStatus, 'created'), + total_events: z._default(z.int(), 0), + replayed_events: z._default(z.int(), 0), + failed_events: z._default(z.int(), 0), + skipped_events: z._default(z.int(), 0), created_at: z.iso.datetime(), - started_at: z.iso.datetime().nullable(), - completed_at: z.iso.datetime().nullable(), - last_event_at: z.iso.datetime().nullable(), + started_at: z.nullable(z.iso.datetime()), + completed_at: z.nullable(z.iso.datetime()), + last_event_at: z.nullable(z.iso.datetime()), errors: z.array(zReplayError) }); @@ -1415,15 +1415,15 @@ export const zReplaySession = z.object({ */ export const zResourceLimitExceededEvent = z.object({ event_id: z.string(), - event_type: z.literal('resource_limit_exceeded').default('resource_limit_exceeded'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('resource_limit_exceeded'), 'resource_limit_exceeded'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - resource_type: z.string().optional(), - limit: z.int().optional(), - requested: z.int().optional(), - user_id: z.string().nullish() + resource_type: z.optional(z.string()), + limit: z.optional(z.int()), + requested: z.optional(z.int()), + user_id: z.nullish(z.string()) }); /** @@ -1447,10 +1447,10 @@ export const zResourceLimits = z.object({ * Model for execution resource usage. */ export const zResourceUsage = z.object({ - execution_time_wall_seconds: z.number().default(0), - cpu_time_jiffies: z.int().default(0), - clk_tck_hertz: z.int().default(0), - peak_memory_kb: z.int().default(0) + execution_time_wall_seconds: z._default(z.number(), 0), + cpu_time_jiffies: z._default(z.int(), 0), + clk_tck_hertz: z._default(z.int(), 0), + peak_memory_kb: z._default(z.int(), 0) }); /** @@ -1461,14 +1461,14 @@ export const zResourceUsage = z.object({ export const zExecutionResult = z.object({ execution_id: z.string(), status: zExecutionStatus, - stdout: z.string().nullable(), - stderr: z.string().nullable(), + stdout: z.nullable(z.string()), + stderr: z.nullable(z.string()), lang: z.string(), lang_version: z.string(), - resource_usage: zResourceUsage.nullable(), - exit_code: z.int().nullable(), - error_type: zExecutionErrorType.nullable(), - priority: zQueuePriority.default('normal') + resource_usage: z.nullable(zResourceUsage), + exit_code: z.nullable(z.int()), + error_type: z.nullable(zExecutionErrorType), + priority: z._default(zQueuePriority, 'normal') }); /** @@ -1485,12 +1485,12 @@ export const zEventReplayStatusResponse = z.object({ skipped_events: z.int(), replay_id: z.string(), created_at: z.iso.datetime(), - started_at: z.iso.datetime().nullable(), - completed_at: z.iso.datetime().nullable(), - errors: z.array(zReplayError).nullable(), - estimated_completion: z.iso.datetime().nullable(), - execution_results: z.array(zExecutionResult).nullable(), - progress_percentage: z.number().readonly() + started_at: z.nullable(z.iso.datetime()), + completed_at: z.nullable(z.iso.datetime()), + errors: z.nullable(z.array(zReplayError)), + estimated_completion: z.nullable(z.iso.datetime()), + execution_results: z.nullable(z.array(zExecutionResult)), + progress_percentage: z.readonly(z.number()) }); /** @@ -1512,10 +1512,10 @@ export const zExecutionListResponse = z.object({ * Resource usage metrics from script execution. */ export const zResourceUsageDomain = z.object({ - execution_time_wall_seconds: z.number().optional().default(0), - cpu_time_jiffies: z.int().optional().default(0), - clk_tck_hertz: z.int().optional().default(0), - peak_memory_kb: z.int().optional().default(0) + execution_time_wall_seconds: z._default(z.optional(z.number()), 0), + cpu_time_jiffies: z._default(z.optional(z.int()), 0), + clk_tck_hertz: z._default(z.optional(z.int()), 0), + peak_memory_kb: z._default(z.optional(z.int()), 0) }); /** @@ -1523,16 +1523,16 @@ export const zResourceUsageDomain = z.object({ */ export const zExecutionCompletedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_completed').default('execution_completed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_completed'), 'execution_completed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - exit_code: z.int().optional(), - resource_usage: zResourceUsageDomain.optional(), - stdout: z.string().optional().default(''), - stderr: z.string().optional().default('') + execution_id: z.optional(z.string()), + exit_code: z.optional(z.int()), + resource_usage: z.optional(zResourceUsageDomain), + stdout: z._default(z.optional(z.string()), ''), + stderr: z._default(z.optional(z.string()), '') }); /** @@ -1540,17 +1540,17 @@ export const zExecutionCompletedEvent = z.object({ */ export const zExecutionFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_failed').default('execution_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_failed'), 'execution_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - exit_code: z.int().optional().default(1), - error_type: zExecutionErrorType.optional(), - resource_usage: zResourceUsageDomain.nullish(), - stdout: z.string().optional().default(''), - stderr: z.string().optional().default('') + execution_id: z.optional(z.string()), + exit_code: z._default(z.optional(z.int()), 1), + error_type: z.optional(zExecutionErrorType), + resource_usage: z.nullish(zResourceUsageDomain), + stdout: z._default(z.optional(z.string()), ''), + stderr: z._default(z.optional(z.string()), '') }); /** @@ -1558,16 +1558,16 @@ export const zExecutionFailedEvent = z.object({ */ export const zExecutionTimeoutEvent = z.object({ event_id: z.string(), - event_type: z.literal('execution_timeout').default('execution_timeout'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('execution_timeout'), 'execution_timeout'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - timeout_seconds: z.int().optional(), - resource_usage: zResourceUsageDomain.optional(), - stdout: z.string().optional().default(''), - stderr: z.string().optional().default('') + execution_id: z.optional(z.string()), + timeout_seconds: z.optional(z.int()), + resource_usage: z.optional(zResourceUsageDomain), + stdout: z._default(z.optional(z.string()), ''), + stderr: z._default(z.optional(z.string()), '') }); /** @@ -1577,7 +1577,7 @@ export const zExecutionTimeoutEvent = z.object({ */ export const zRestoreSettingsRequest = z.object({ timestamp: z.iso.datetime(), - reason: z.string().nullish() + reason: z.nullish(z.string()) }); /** @@ -1602,10 +1602,10 @@ export const zSseExecutionEventSchema = z.object({ zSseControlEvent ]), execution_id: z.string(), - timestamp: z.iso.datetime().nullable(), - event_id: z.string().nullable(), - status: zExecutionStatus.nullable(), - result: zExecutionResult.nullable() + timestamp: z.nullable(z.iso.datetime()), + event_id: z.nullable(z.string()), + status: z.nullable(zExecutionStatus), + result: z.nullable(zExecutionResult) }); /** @@ -1624,19 +1624,19 @@ export const zSagaCancellationResponse = z.object({ */ export const zSagaCancelledEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_cancelled').default('saga_cancelled'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_cancelled'), 'saga_cancelled'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - reason: z.string().optional(), - completed_steps: z.array(z.string()).optional(), - compensated_steps: z.array(z.string()).optional(), - cancelled_at: z.iso.datetime().nullish(), - cancelled_by: z.string().nullish() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + reason: z.optional(z.string()), + completed_steps: z.optional(z.array(z.string())), + compensated_steps: z.optional(z.array(z.string())), + cancelled_at: z.nullish(z.iso.datetime()), + cancelled_by: z.nullish(z.string()) }); /** @@ -1644,15 +1644,15 @@ export const zSagaCancelledEvent = z.object({ */ export const zSagaCompensatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_compensated').default('saga_compensated'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_compensated'), 'saga_compensated'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - compensated_steps: z.array(z.string()).optional() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + compensated_steps: z.optional(z.array(z.string())) }); /** @@ -1660,15 +1660,15 @@ export const zSagaCompensatedEvent = z.object({ */ export const zSagaCompensatingEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_compensating').default('saga_compensating'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_compensating'), 'saga_compensating'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - compensating_step: z.string().optional() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + compensating_step: z.optional(z.string()) }); /** @@ -1676,15 +1676,15 @@ export const zSagaCompensatingEvent = z.object({ */ export const zSagaCompletedEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_completed').default('saga_completed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_completed'), 'saga_completed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - completed_steps: z.array(z.string()).optional() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + completed_steps: z.optional(z.array(z.string())) }); /** @@ -1692,16 +1692,16 @@ export const zSagaCompletedEvent = z.object({ */ export const zSagaFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_failed').default('saga_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_failed'), 'saga_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - failed_step: z.string().optional(), - error: z.string().optional() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + failed_step: z.optional(z.string()), + error: z.optional(z.string()) }); /** @@ -1709,15 +1709,15 @@ export const zSagaFailedEvent = z.object({ */ export const zSagaStartedEvent = z.object({ event_id: z.string(), - event_type: z.literal('saga_started').default('saga_started'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('saga_started'), 'saga_started'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - saga_id: z.string().optional(), - saga_name: z.string().optional(), - execution_id: z.string().optional(), - initial_event_id: z.string().optional() + saga_id: z.optional(z.string()), + saga_name: z.optional(z.string()), + execution_id: z.optional(z.string()), + initial_event_id: z.optional(z.string()) }); /** @@ -1745,13 +1745,13 @@ export const zSagaStatusResponse = z.object({ saga_name: z.string(), execution_id: z.string(), state: zSagaState, - current_step: z.string().nullable(), + current_step: z.nullable(z.string()), completed_steps: z.array(z.string()), compensated_steps: z.array(z.string()), - error_message: z.string().nullable(), + error_message: z.nullable(z.string()), created_at: z.iso.datetime(), updated_at: z.iso.datetime(), - completed_at: z.iso.datetime().nullable(), + completed_at: z.nullable(z.iso.datetime()), retry_count: z.int() }); @@ -1774,9 +1774,9 @@ export const zSagaListResponse = z.object({ export const zSavedScriptCreateRequest = z.object({ name: z.string(), script: z.string(), - lang: z.string().optional().default('python'), - lang_version: z.string().optional().default('3.14'), - description: z.string().nullish() + lang: z._default(z.optional(z.string()), 'python'), + lang_version: z._default(z.optional(z.string()), '3.14'), + description: z.nullish(z.string()) }); /** @@ -1788,7 +1788,7 @@ export const zSavedScriptResponse = z.object({ script: z.string(), lang: z.string(), lang_version: z.string(), - description: z.string().nullable(), + description: z.nullable(z.string()), created_at: z.iso.datetime(), updated_at: z.iso.datetime() }); @@ -1804,12 +1804,12 @@ export const zSavedScriptListResponse = z.object({ * SavedScriptUpdate */ export const zSavedScriptUpdate = z.object({ - name: z.string().nullish(), - script: z.string().nullish(), - lang: z.string().nullish(), - lang_version: z.string().nullish(), - description: z.string().nullish(), - updated_at: z.iso.datetime().optional() + name: z.nullish(z.string()), + script: z.nullish(z.string()), + lang: z.nullish(z.string()), + lang_version: z.nullish(z.string()), + description: z.nullish(z.string()), + updated_at: z.optional(z.iso.datetime()) }); /** @@ -1817,14 +1817,14 @@ export const zSavedScriptUpdate = z.object({ */ export const zScriptDeletedEvent = z.object({ event_id: z.string(), - event_type: z.literal('script_deleted').default('script_deleted'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('script_deleted'), 'script_deleted'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - script_id: z.string().optional(), - user_id: z.string().optional(), - deleted_by: z.string().nullish() + script_id: z.optional(z.string()), + user_id: z.optional(z.string()), + deleted_by: z.nullish(z.string()) }); /** @@ -1832,15 +1832,15 @@ export const zScriptDeletedEvent = z.object({ */ export const zScriptSavedEvent = z.object({ event_id: z.string(), - event_type: z.literal('script_saved').default('script_saved'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('script_saved'), 'script_saved'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - script_id: z.string().optional(), - user_id: z.string().optional(), - title: z.string().optional(), - language: z.string().optional() + script_id: z.optional(z.string()), + user_id: z.optional(z.string()), + title: z.optional(z.string()), + language: z.optional(z.string()) }); /** @@ -1848,15 +1848,15 @@ export const zScriptSavedEvent = z.object({ */ export const zScriptSharedEvent = z.object({ event_id: z.string(), - event_type: z.literal('script_shared').default('script_shared'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('script_shared'), 'script_shared'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - script_id: z.string().optional(), - shared_by: z.string().optional(), - shared_with: z.array(z.string()).optional(), - permissions: z.string().optional() + script_id: z.optional(z.string()), + shared_by: z.optional(z.string()), + shared_with: z.optional(z.array(z.string())), + permissions: z.optional(z.string()) }); /** @@ -1864,15 +1864,15 @@ export const zScriptSharedEvent = z.object({ */ export const zSecurityViolationEvent = z.object({ event_id: z.string(), - event_type: z.literal('security_violation').default('security_violation'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('security_violation'), 'security_violation'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().nullish(), - violation_type: z.string().optional(), - details: z.string().optional(), - ip_address: z.string().nullish() + user_id: z.nullish(z.string()), + violation_type: z.optional(z.string()), + details: z.optional(z.string()), + ip_address: z.nullish(z.string()) }); /** @@ -1888,14 +1888,14 @@ export const zServiceEventCount = z.object({ */ export const zServiceRecoveredEvent = z.object({ event_id: z.string(), - event_type: z.literal('service_recovered').default('service_recovered'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('service_recovered'), 'service_recovered'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - service_name: z.string().optional(), - health_check: z.string().optional(), - downtime_seconds: z.int().optional() + service_name: z.optional(z.string()), + health_check: z.optional(z.string()), + downtime_seconds: z.optional(z.int()) }); /** @@ -1903,14 +1903,14 @@ export const zServiceRecoveredEvent = z.object({ */ export const zServiceUnhealthyEvent = z.object({ event_id: z.string(), - event_type: z.literal('service_unhealthy').default('service_unhealthy'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('service_unhealthy'), 'service_unhealthy'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - service_name: z.string().optional(), - health_check: z.string().optional(), - reason: z.string().optional() + service_name: z.optional(z.string()), + health_check: z.optional(z.string()), + reason: z.optional(z.string()) }); /** @@ -1937,9 +1937,9 @@ export const zSessionSummary = z.object({ failed_events: z.int(), skipped_events: z.int(), created_at: z.iso.datetime(), - started_at: z.iso.datetime().nullable(), - completed_at: z.iso.datetime().nullable(), - duration_seconds: z.number().nullable() + started_at: z.nullable(z.iso.datetime()), + completed_at: z.nullable(z.iso.datetime()), + duration_seconds: z.nullable(z.number()) }); /** @@ -1953,7 +1953,7 @@ export const zSettingsHistoryEntry = z.object({ field: z.string(), old_value: z.unknown(), new_value: z.unknown(), - reason: z.string().nullable() + reason: z.nullable(z.string()) }); /** @@ -1983,13 +1983,13 @@ export const zStorageType = z.enum([ */ export const zResultFailedEvent = z.object({ event_id: z.string(), - event_type: z.literal('result_failed').default('result_failed'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('result_failed'), 'result_failed'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - storage_type: zStorageType.nullish() + execution_id: z.optional(z.string()), + storage_type: z.nullish(zStorageType) }); /** @@ -1997,15 +1997,15 @@ export const zResultFailedEvent = z.object({ */ export const zResultStoredEvent = z.object({ event_id: z.string(), - event_type: z.literal('result_stored').default('result_stored'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('result_stored'), 'result_stored'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - execution_id: z.string().optional(), - storage_type: zStorageType.nullish(), - storage_path: z.string().optional().default(''), - size_bytes: z.int().optional().default(0) + execution_id: z.optional(z.string()), + storage_type: z.nullish(zStorageType), + storage_path: z._default(z.optional(z.string()), ''), + size_bytes: z._default(z.optional(z.int()), 0) }); /** @@ -2015,16 +2015,16 @@ export const zResultStoredEvent = z.object({ */ export const zSubscriptionUpdate = z.object({ enabled: z.boolean(), - severities: z.array(zNotificationSeverity).optional(), - include_tags: z.array(z.string()).optional(), - exclude_tags: z.array(z.string()).optional(), - webhook_url: z.string().regex(/^https:\/\//).nullish(), - slack_webhook: z.string().regex(/^https:\/\/hooks\.slack\.com\//).nullish(), - quiet_hours_enabled: z.boolean().optional().default(false), - quiet_hours_start: z.string().nullish(), - quiet_hours_end: z.string().nullish(), - timezone: z.string().optional().default('UTC'), - batch_interval_minutes: z.int().optional().default(60) + severities: z.optional(z.array(zNotificationSeverity)), + include_tags: z.optional(z.array(z.string())), + exclude_tags: z.optional(z.array(z.string())), + webhook_url: z.nullish(z.string().check(z.regex(/^https:\/\//))), + slack_webhook: z.nullish(z.string().check(z.regex(/^https:\/\/hooks\.slack\.com\//))), + quiet_hours_enabled: z._default(z.optional(z.boolean()), false), + quiet_hours_start: z.nullish(z.string()), + quiet_hours_end: z.nullish(z.string()), + timezone: z._default(z.optional(z.string()), 'UTC'), + batch_interval_minutes: z._default(z.optional(z.int()), 60) }); /** @@ -2041,15 +2041,15 @@ export const zSubscriptionsResponse = z.object({ */ export const zSystemErrorEvent = z.object({ event_id: z.string(), - event_type: z.literal('system_error').default('system_error'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('system_error'), 'system_error'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - error_type: z.string().optional(), - message: z.string().optional(), - service_name: z.string().optional(), - stack_trace: z.string().nullish() + error_type: z.optional(z.string()), + message: z.optional(z.string()), + service_name: z.optional(z.string()), + stack_trace: z.nullish(z.string()) }); /** @@ -2058,19 +2058,19 @@ export const zSystemErrorEvent = z.object({ * API schema for system settings with validation. */ export const zSystemSettingsSchemaInput = z.object({ - max_timeout_seconds: z.int().gte(1).lte(3600).optional().default(300), - memory_limit: z.string().optional().default('512Mi'), - cpu_limit: z.string().optional().default('2000m'), - max_concurrent_executions: z.int().gte(1).lte(100).optional().default(10), - max_executions_per_user: z.int().gte(1).lte(1000).optional().default(100), - password_min_length: z.int().gte(8).lte(32).optional().default(8), - session_timeout_minutes: z.int().gte(5).lte(1440).optional().default(60), - max_login_attempts: z.int().gte(3).lte(10).optional().default(5), - lockout_duration_minutes: z.int().gte(5).lte(60).optional().default(15), - metrics_retention_days: z.int().gte(7).lte(90).optional().default(30), - log_level: zLogLevel.optional().default('INFO'), - enable_tracing: z.boolean().optional().default(true), - sampling_rate: z.number().gte(0).lte(1).optional().default(0.1) + max_timeout_seconds: z._default(z.optional(z.int().check(z.gte(1), z.lte(3600))), 300), + memory_limit: z._default(z.optional(z.string()), '512Mi'), + cpu_limit: z._default(z.optional(z.string()), '2000m'), + max_concurrent_executions: z._default(z.optional(z.int().check(z.gte(1), z.lte(100))), 10), + max_executions_per_user: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), + password_min_length: z._default(z.optional(z.int().check(z.gte(8), z.lte(32))), 8), + session_timeout_minutes: z._default(z.optional(z.int().check(z.gte(5), z.lte(1440))), 60), + max_login_attempts: z._default(z.optional(z.int().check(z.gte(3), z.lte(10))), 5), + lockout_duration_minutes: z._default(z.optional(z.int().check(z.gte(5), z.lte(60))), 15), + metrics_retention_days: z._default(z.optional(z.int().check(z.gte(7), z.lte(90))), 30), + log_level: z._default(z.optional(zLogLevel), 'INFO'), + enable_tracing: z._default(z.optional(z.boolean()), true), + sampling_rate: z._default(z.optional(z.number().check(z.gte(0), z.lte(1))), 0.1) }); /** @@ -2079,19 +2079,19 @@ export const zSystemSettingsSchemaInput = z.object({ * API schema for system settings with validation. */ export const zSystemSettingsSchemaOutput = z.object({ - max_timeout_seconds: z.int().gte(1).lte(3600).default(300), - memory_limit: z.string().default('512Mi'), - cpu_limit: z.string().default('2000m'), - max_concurrent_executions: z.int().gte(1).lte(100).default(10), - max_executions_per_user: z.int().gte(1).lte(1000).default(100), - password_min_length: z.int().gte(8).lte(32).default(8), - session_timeout_minutes: z.int().gte(5).lte(1440).default(60), - max_login_attempts: z.int().gte(3).lte(10).default(5), - lockout_duration_minutes: z.int().gte(5).lte(60).default(15), - metrics_retention_days: z.int().gte(7).lte(90).default(30), - log_level: zLogLevel.default('INFO'), - enable_tracing: z.boolean().default(true), - sampling_rate: z.number().gte(0).lte(1).default(0.1) + max_timeout_seconds: z._default(z.int().check(z.gte(1), z.lte(3600)), 300), + memory_limit: z._default(z.string(), '512Mi'), + cpu_limit: z._default(z.string(), '2000m'), + max_concurrent_executions: z._default(z.int().check(z.gte(1), z.lte(100)), 10), + max_executions_per_user: z._default(z.int().check(z.gte(1), z.lte(1000)), 100), + password_min_length: z._default(z.int().check(z.gte(8), z.lte(32)), 8), + session_timeout_minutes: z._default(z.int().check(z.gte(5), z.lte(1440)), 60), + max_login_attempts: z._default(z.int().check(z.gte(3), z.lte(10)), 5), + lockout_duration_minutes: z._default(z.int().check(z.gte(5), z.lte(60)), 15), + metrics_retention_days: z._default(z.int().check(z.gte(7), z.lte(90)), 30), + log_level: z._default(zLogLevel, 'INFO'), + enable_tracing: z._default(z.boolean(), true), + sampling_rate: z._default(z.number().check(z.gte(0), z.lte(1)), 0.1) }); /** @@ -2138,14 +2138,14 @@ export const zUnreadCountResponse = z.object({ */ export const zUserDeletedEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_deleted').default('user_deleted'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_deleted'), 'user_deleted'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - deleted_by: z.string().nullish(), - reason: z.string().nullish() + user_id: z.optional(z.string()), + deleted_by: z.nullish(z.string()), + reason: z.nullish(z.string()) }); /** @@ -2167,10 +2167,10 @@ export const zEventStatistics = z.object({ events_by_service: z.array(zServiceEventCount), events_by_hour: z.array(zHourlyEventCount), top_users: z.array(zUserEventCount), - error_rate: z.number().default(0), - avg_processing_time: z.number().default(0), - start_time: z.iso.datetime().nullable(), - end_time: z.iso.datetime().nullable() + error_rate: z._default(z.number(), 0), + avg_processing_time: z._default(z.number(), 0), + start_time: z.nullable(z.iso.datetime()), + end_time: z.nullable(z.iso.datetime()) }); /** @@ -2192,15 +2192,15 @@ export const zEventStatsResponse = z.object({ */ export const zUserLoggedInEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_logged_in').default('user_logged_in'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_logged_in'), 'user_logged_in'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - login_method: zLoginMethod.optional(), - ip_address: z.string().nullish(), - user_agent: z.string().nullish() + user_id: z.optional(z.string()), + login_method: z.optional(zLoginMethod), + ip_address: z.nullish(z.string()), + user_agent: z.nullish(z.string()) }); /** @@ -2208,13 +2208,13 @@ export const zUserLoggedInEvent = z.object({ */ export const zUserLoggedOutEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_logged_out').default('user_logged_out'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_logged_out'), 'user_logged_out'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - logout_reason: z.string().nullish() + user_id: z.optional(z.string()), + logout_reason: z.nullish(z.string()) }); /** @@ -2222,15 +2222,15 @@ export const zUserLoggedOutEvent = z.object({ */ export const zUserLoginEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_login').default('user_login'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_login'), 'user_login'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - login_method: zLoginMethod.optional(), - ip_address: z.string().nullish(), - user_agent: z.string().nullish() + user_id: z.optional(z.string()), + login_method: z.optional(zLoginMethod), + ip_address: z.nullish(z.string()), + user_agent: z.nullish(z.string()) }); /** @@ -2243,9 +2243,9 @@ export const zUserRateLimitConfigResponse = z.object({ bypass_rate_limit: z.boolean(), global_multiplier: z.number(), rules: z.array(zRateLimitRuleResponse), - created_at: z.iso.datetime().nullable(), - updated_at: z.iso.datetime().nullable(), - notes: z.string().nullable() + created_at: z.nullable(z.iso.datetime()), + updated_at: z.nullable(z.iso.datetime()), + notes: z.nullable(z.string()) }); /** @@ -2266,7 +2266,7 @@ export const zRateLimitUpdateResponse = z.object({ */ export const zUserRateLimitsResponse = z.object({ user_id: z.string(), - rate_limit_config: zUserRateLimitConfigResponse.nullable(), + rate_limit_config: z.nullable(zUserRateLimitConfigResponse), current_usage: z.record(z.string(), zEndpointUsageStats) }); @@ -2275,14 +2275,14 @@ export const zUserRateLimitsResponse = z.object({ */ export const zUserRegisteredEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_registered').default('user_registered'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_registered'), 'user_registered'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - username: z.string().optional(), - email: z.string().optional() + user_id: z.optional(z.string()), + username: z.optional(z.string()), + email: z.optional(z.string()) }); /** @@ -2316,9 +2316,9 @@ export const zLoginResponse = z.object({ export const zUserCreate = z.object({ username: z.string(), email: z.email(), - role: zUserRole.optional().default('user'), - is_active: z.boolean().optional().default(true), - password: z.string().min(8) + role: z._default(z.optional(zUserRole), 'user'), + is_active: z._default(z.optional(z.boolean()), true), + password: z.string().check(z.minLength(8)) }); /** @@ -2335,9 +2335,9 @@ export const zUserResponse = z.object({ is_superuser: z.boolean(), created_at: z.iso.datetime(), updated_at: z.iso.datetime(), - bypass_rate_limit: z.boolean().nullable(), - global_multiplier: z.number().nullable(), - has_custom_limits: z.boolean().nullable() + bypass_rate_limit: z.nullable(z.boolean()), + global_multiplier: z.nullable(z.number()), + has_custom_limits: z.nullable(z.boolean()) }); /** @@ -2359,14 +2359,14 @@ export const zUserListResponse = z.object({ */ export const zUserSettings = z.object({ user_id: z.string(), - theme: zTheme.default('auto'), - timezone: z.string().default('UTC'), - date_format: z.string().default('YYYY-MM-DD'), - time_format: z.string().default('24h'), + theme: z._default(zTheme, 'auto'), + timezone: z._default(z.string(), 'UTC'), + date_format: z._default(z.string(), 'YYYY-MM-DD'), + time_format: z._default(z.string(), '24h'), notifications: zNotificationSettingsOutput, editor: zEditorSettingsOutput, custom_settings: z.record(z.string(), z.unknown()), - version: z.int().default(1), + version: z._default(z.int(), 1), created_at: z.iso.datetime(), updated_at: z.iso.datetime() }); @@ -2377,13 +2377,13 @@ export const zUserSettings = z.object({ * Partial update model for user settings */ export const zUserSettingsUpdate = z.object({ - theme: zTheme.nullish(), - timezone: z.string().nullish(), - date_format: z.string().nullish(), - time_format: z.string().nullish(), - notifications: zNotificationSettingsInput.nullish(), - editor: zEditorSettingsInput.nullish(), - custom_settings: z.record(z.string(), z.unknown()).nullish() + theme: z.nullish(zTheme), + timezone: z.nullish(z.string()), + date_format: z.nullish(z.string()), + time_format: z.nullish(z.string()), + notifications: z.nullish(zNotificationSettingsInput), + editor: z.nullish(zEditorSettingsInput), + custom_settings: z.nullish(z.record(z.string(), z.unknown())) }); /** @@ -2391,14 +2391,14 @@ export const zUserSettingsUpdate = z.object({ */ export const zUserSettingsUpdatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_settings_updated').default('user_settings_updated'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_settings_updated'), 'user_settings_updated'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - changed_fields: z.array(z.string()).optional(), - reason: z.string().nullish() + user_id: z.optional(z.string()), + changed_fields: z.optional(z.array(z.string())), + reason: z.nullish(z.string()) }); /** @@ -2407,11 +2407,11 @@ export const zUserSettingsUpdatedEvent = z.object({ * Model for updating a user */ export const zUserUpdate = z.object({ - username: z.string().nullish(), - email: z.email().nullish(), - role: zUserRole.nullish(), - is_active: z.boolean().nullish(), - password: z.string().min(8).nullish() + username: z.nullish(z.string()), + email: z.nullish(z.email()), + role: z.nullish(zUserRole), + is_active: z.nullish(z.boolean()), + password: z.nullish(z.string().check(z.minLength(8))) }); /** @@ -2419,14 +2419,14 @@ export const zUserUpdate = z.object({ */ export const zUserUpdatedEvent = z.object({ event_id: z.string(), - event_type: z.literal('user_updated').default('user_updated'), - event_version: z.string().default('1.0'), + event_type: z._default(z.literal('user_updated'), 'user_updated'), + event_version: z._default(z.string(), '1.0'), timestamp: z.iso.datetime(), - aggregate_id: z.string().nullish(), + aggregate_id: z.nullish(z.string()), metadata: zEventMetadata, - user_id: z.string().optional(), - updated_fields: z.array(z.string()).optional(), - updated_by: z.string().nullish() + user_id: z.optional(z.string()), + updated_fields: z.optional(z.array(z.string())), + updated_by: z.nullish(z.string()) }); /** @@ -2437,7 +2437,7 @@ export const zAdminUserOverview = z.object({ stats: zEventStatistics, derived_counts: zDerivedCounts, rate_limit_summary: zRateLimitSummary, - recent_events: z.array(z.union([ + recent_events: z._default(z.array(z.union([ zExecutionRequestedEvent, zExecutionAcceptedEvent, zExecutionQueuedEvent, @@ -2495,7 +2495,7 @@ export const zAdminUserOverview = z.object({ zDlqMessageReceivedEvent, zDlqMessageRetriedEvent, zDlqMessageDiscardedEvent - ])).default([]) + ])), []) }); /** @@ -2644,15 +2644,15 @@ export const zValidationError = z.object({ loc: z.array(z.union([z.string(), z.int()])), msg: z.string(), type: z.string(), - input: z.unknown().optional(), - ctx: z.record(z.string(), z.unknown()).optional() + input: z.optional(z.unknown()), + ctx: z.optional(z.record(z.string(), z.unknown())) }); /** * HTTPValidationError */ export const zHttpValidationError = z.object({ - detail: z.array(zValidationError).optional() + detail: z.optional(z.array(zValidationError)) }); /** @@ -2669,11 +2669,11 @@ export const zEventReplayStatusResponseWritable = z.object({ skipped_events: z.int(), replay_id: z.string(), created_at: z.iso.datetime(), - started_at: z.iso.datetime().nullable(), - completed_at: z.iso.datetime().nullable(), - errors: z.array(zReplayError).nullable(), - estimated_completion: z.iso.datetime().nullable(), - execution_results: z.array(zExecutionResult).nullable() + started_at: z.nullable(z.iso.datetime()), + completed_at: z.nullable(z.iso.datetime()), + errors: z.nullable(z.array(zReplayError)), + estimated_completion: z.nullable(z.iso.datetime()), + execution_results: z.nullable(z.array(zExecutionResult)) }); /** @@ -2690,8 +2690,8 @@ export const zSessionSummaryWritable = z.object({ failed_events: z.int(), skipped_events: z.int(), created_at: z.iso.datetime(), - started_at: z.iso.datetime().nullable(), - completed_at: z.iso.datetime().nullable() + started_at: z.nullable(z.iso.datetime()), + completed_at: z.nullable(z.iso.datetime()) }); export const zLoginApiV1AuthLoginPostBody = zBodyLoginApiV1AuthLoginPost; @@ -2721,7 +2721,7 @@ export const zLogoutApiV1AuthLogoutPostResponse = zMessageResponse; export const zCreateExecutionApiV1ExecutePostBody = zExecutionRequest; export const zCreateExecutionApiV1ExecutePostHeaders = z.object({ - 'Idempotency-Key': z.string().nullish() + 'Idempotency-Key': z.nullish(z.string()) }); /** @@ -2763,8 +2763,8 @@ export const zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetPath = z.obje }); export const zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetQuery = z.object({ - event_types: z.array(zEventType).nullish(), - limit: z.int().gte(1).lte(1000).optional().default(100) + event_types: z.nullish(z.array(zEventType)), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100) }); /** @@ -2833,12 +2833,12 @@ export const zGetExecutionEventsApiV1ExecutionsExecutionIdEventsGetResponse = z. ])); export const zGetUserExecutionsApiV1UserExecutionsGetQuery = z.object({ - status: zExecutionStatus.nullish(), - lang: z.string().nullish(), - start_time: z.iso.datetime().nullish(), - end_time: z.iso.datetime().nullish(), - limit: z.int().gte(1).lte(200).optional().default(50), - skip: z.int().gte(0).optional().default(0) + status: z.nullish(zExecutionStatus), + lang: z.nullish(z.string()), + start_time: z.nullish(z.iso.datetime()), + end_time: z.nullish(z.iso.datetime()), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(200))), 50), + skip: z._default(z.optional(z.int().check(z.gte(0))), 0) }); /** @@ -2907,8 +2907,8 @@ export const zUpdateSavedScriptApiV1ScriptsScriptIdPutPath = z.object({ export const zUpdateSavedScriptApiV1ScriptsScriptIdPutResponse = zSavedScriptResponse; export const zListReplaySessionsApiV1ReplaySessionsGetQuery = z.object({ - status: zReplayStatus.nullish(), - limit: z.int().gte(1).lte(1000).optional().default(100) + status: z.nullish(zReplayStatus), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100) }); /** @@ -2971,7 +2971,7 @@ export const zGetReplaySessionApiV1ReplaySessionsSessionIdGetPath = z.object({ export const zGetReplaySessionApiV1ReplaySessionsSessionIdGetResponse = zReplaySession; export const zCleanupOldSessionsApiV1ReplayCleanupPostQuery = z.object({ - older_than_hours: z.int().gte(1).optional().default(24) + older_than_hours: z._default(z.optional(z.int().check(z.gte(1))), 24) }); /** @@ -3006,7 +3006,7 @@ export const zBrowseEventsApiV1AdminEventsBrowsePostBody = zEventBrowseRequest; export const zBrowseEventsApiV1AdminEventsBrowsePostResponse = zEventBrowseResponse; export const zGetEventStatsApiV1AdminEventsStatsGetQuery = z.object({ - hours: z.int().gte(1).lte(168).optional().default(24) + hours: z._default(z.optional(z.int().check(z.gte(1), z.lte(168))), 24) }); /** @@ -3019,13 +3019,13 @@ export const zExportEventsApiV1AdminEventsExportExportFormatGetPath = z.object({ }); export const zExportEventsApiV1AdminEventsExportExportFormatGetQuery = z.object({ - event_types: z.array(zEventType).nullish(), - aggregate_id: z.string().nullish(), - user_id: z.string().nullish(), - service_name: z.string().nullish(), - start_time: z.iso.datetime().nullish(), - end_time: z.iso.datetime().nullish(), - limit: z.int().gte(1).lte(50000).optional().default(10000) + event_types: z.nullish(z.array(zEventType)), + aggregate_id: z.nullish(z.string()), + user_id: z.nullish(z.string()), + service_name: z.nullish(z.string()), + start_time: z.nullish(z.iso.datetime()), + end_time: z.nullish(z.iso.datetime()), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(50000))), 10000) }); export const zDeleteEventApiV1AdminEventsEventIdDeletePath = z.object({ @@ -3063,11 +3063,11 @@ export const zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetPath = z export const zStreamReplayStatusApiV1AdminEventsReplaySessionIdStatusGetResponse = zEventReplayStatusResponse; export const zListExecutionsApiV1AdminExecutionsGetQuery = z.object({ - status: zExecutionStatus.nullish(), - priority: zQueuePriority.nullish(), - user_id: z.string().nullish(), - limit: z.int().gte(1).lte(200).optional().default(50), - skip: z.int().gte(0).optional().default(0) + status: z.nullish(zExecutionStatus), + priority: z.nullish(zQueuePriority), + user_id: z.nullish(z.string()), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(200))), 50), + skip: z._default(z.optional(z.int().check(z.gte(0))), 0) }); /** @@ -3145,10 +3145,10 @@ export const zUpdateSystemSettingsApiV1AdminSettingsPutResponse = zSystemSetting export const zResetSystemSettingsApiV1AdminSettingsResetPostResponse = zSystemSettingsSchemaOutput; export const zListUsersApiV1AdminUsersGetQuery = z.object({ - limit: z.int().gte(1).lte(1000).optional().default(100), - offset: z.int().gte(0).optional().default(0), - search: z.string().nullish(), - role: zUserRole.nullish() + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), + offset: z._default(z.optional(z.int().check(z.gte(0))), 0), + search: z.nullish(z.string()), + role: z.nullish(zUserRole) }); /** @@ -3168,7 +3168,7 @@ export const zDeleteUserApiV1AdminUsersUserIdDeletePath = z.object({ }); export const zDeleteUserApiV1AdminUsersUserIdDeleteQuery = z.object({ - cascade: z.boolean().optional().default(true) + cascade: z._default(z.optional(z.boolean()), true) }); /** @@ -3259,7 +3259,7 @@ export const zUpdateEditorSettingsApiV1UserSettingsEditorPutBody = zEditorSettin export const zUpdateEditorSettingsApiV1UserSettingsEditorPutResponse = zUserSettings; export const zGetSettingsHistoryApiV1UserSettingsHistoryGetQuery = z.object({ - limit: z.int().gte(1).lte(200).optional().default(50) + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(200))), 50) }); /** @@ -3289,12 +3289,12 @@ export const zUpdateCustomSettingApiV1UserSettingsCustomKeyPutPath = z.object({ export const zUpdateCustomSettingApiV1UserSettingsCustomKeyPutResponse = zUserSettings; export const zGetNotificationsApiV1NotificationsGetQuery = z.object({ - status: zNotificationStatus.nullish(), - include_tags: z.array(z.string()).nullish(), - exclude_tags: z.array(z.string()).nullish(), - tag_prefix: z.string().nullish(), - limit: z.int().gte(1).lte(100).optional().default(50), - offset: z.int().gte(0).optional().default(0) + status: z.nullish(zNotificationStatus), + include_tags: z.nullish(z.array(z.string())), + exclude_tags: z.nullish(z.array(z.string())), + tag_prefix: z.nullish(z.string()), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(100))), 50), + offset: z._default(z.optional(z.int().check(z.gte(0))), 0) }); /** @@ -3360,9 +3360,9 @@ export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetPath = z.object( }); export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetQuery = z.object({ - state: zSagaState.nullish(), - limit: z.int().gte(1).lte(1000).optional().default(100), - skip: z.int().gte(0).optional().default(0) + state: z.nullish(zSagaState), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), + skip: z._default(z.optional(z.int().check(z.gte(0))), 0) }); /** @@ -3371,10 +3371,10 @@ export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetQuery = z.object export const zGetExecutionSagasApiV1SagasExecutionExecutionIdGetResponse = zSagaListResponse; export const zListSagasApiV1SagasGetQuery = z.object({ - state: zSagaState.nullish(), - execution_id: z.string().nullish(), - limit: z.int().gte(1).lte(1000).optional().default(100), - skip: z.int().gte(0).optional().default(0) + state: z.nullish(zSagaState), + execution_id: z.nullish(z.string()), + limit: z._default(z.optional(z.int().check(z.gte(1), z.lte(1000))), 100), + skip: z._default(z.optional(z.int().check(z.gte(0))), 0) }); /** diff --git a/frontend/src/lib/auth-guard.ts b/frontend/src/lib/auth-guard.ts new file mode 100644 index 000000000..6748ecd31 --- /dev/null +++ b/frontend/src/lib/auth-guard.ts @@ -0,0 +1,26 @@ +import { redirect } from '@sveltejs/kit'; +import { toast } from 'svelte-sonner'; +import { UserRole } from '#lib/api/index.ts'; +import { authStore } from '#stores/auth.svelte.ts'; + +/** + * Send a signed-out visitor to /login, remembering where they were going. + * @param url - The URL being loaded + */ +export async function requireAuth(url: URL): Promise { + const signedIn = await authStore.waitForInit().catch(() => false); + if (signedIn && authStore.isAuthenticated) return; + sessionStorage.setItem('redirectAfterLogin', url.pathname + url.search); + redirect(307, '/login'); +} + +/** + * Like requireAuth, and also send a signed-in non-admin home, before the page fires any admin API call. + * @param url - The URL being loaded + */ +export async function requireAdmin(url: URL): Promise { + await requireAuth(url); + if (authStore.userRole === UserRole.ADMIN) return; + toast.error('Admin access required'); + redirect(307, '/'); +} diff --git a/frontend/src/lib/editor/__tests__/execution.test.ts b/frontend/src/lib/editor/__tests__/execution.test.ts index 1751337b4..5cbf47641 100644 --- a/frontend/src/lib/editor/__tests__/execution.test.ts +++ b/frontend/src/lib/editor/__tests__/execution.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { ExecutionResult } from '$lib/api'; -import { createExecutionApiV1ExecutePost, executionEventsApiV1EventsExecutionsExecutionIdGet } from '$lib/api'; -import { createExecutionState } from '$lib/editor/execution.svelte'; +import type { ExecutionResult } from '#lib/api/index.ts'; +import { createExecutionApiV1ExecutePost, executionEventsApiV1EventsExecutionsExecutionIdGet } from '#lib/api/index.ts'; +import { createExecutionState } from '#lib/editor/execution.svelte.ts'; const mockCreateExecution = vi.mocked(createExecutionApiV1ExecutePost); const mockSseFn = vi.mocked(executionEventsApiV1EventsExecutionsExecutionIdGet); diff --git a/frontend/src/lib/editor/__tests__/languages.test.ts b/frontend/src/lib/editor/__tests__/languages.test.ts index 2050cb2e2..7696417bb 100644 --- a/frontend/src/lib/editor/__tests__/languages.test.ts +++ b/frontend/src/lib/editor/__tests__/languages.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { getLanguageExtension } from '$lib/editor/languages'; +import { getLanguageExtension } from '#lib/editor/languages.ts'; describe('getLanguageExtension', () => { it.each(['python', 'node', 'go', 'ruby', 'bash'])('returns an extension for "%s"', (lang) => { diff --git a/frontend/src/lib/editor/__tests__/runtimes.test.ts b/frontend/src/lib/editor/__tests__/runtimes.test.ts index aa89c7860..c9afa4e39 100644 --- a/frontend/src/lib/editor/__tests__/runtimes.test.ts +++ b/frontend/src/lib/editor/__tests__/runtimes.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { newestVersion } from '$lib/editor/runtimes'; +import { newestVersion } from '#lib/editor/runtimes.ts'; describe('newestVersion', () => { it.each([ diff --git a/frontend/src/lib/editor/execution.svelte.ts b/frontend/src/lib/editor/execution.svelte.ts index 4a6e8be6d..519a60465 100644 --- a/frontend/src/lib/editor/execution.svelte.ts +++ b/frontend/src/lib/editor/execution.svelte.ts @@ -5,8 +5,8 @@ import { type ExecutionStatus, type EventType, type SseControlEvent, -} from '$lib/api'; -import { getErrorMessage } from '$lib/api-interceptors'; +} from '#lib/api/index.ts'; +import { getErrorMessage } from '#lib/api-interceptors.ts'; export type ExecutionPhase = 'idle' | 'starting' | 'queued' | 'scheduled' | 'running'; diff --git a/frontend/src/lib/editor/index.ts b/frontend/src/lib/editor/index.ts index 13b452c6b..4835d9f93 100644 --- a/frontend/src/lib/editor/index.ts +++ b/frontend/src/lib/editor/index.ts @@ -1,3 +1,3 @@ -export { getLanguageExtension } from '$lib/editor/languages'; -export { newestVersion } from '$lib/editor/runtimes'; -export { createExecutionState, type ExecutionState, type ExecutionPhase } from '$lib/editor/execution.svelte'; +export { getLanguageExtension } from '#lib/editor/languages.ts'; +export { newestVersion } from '#lib/editor/runtimes.ts'; +export { createExecutionState, type ExecutionState, type ExecutionPhase } from '#lib/editor/execution.svelte.ts'; diff --git a/frontend/src/lib/logger.ts b/frontend/src/lib/logger.ts index 486c84b21..63a8196d3 100644 --- a/frontend/src/lib/logger.ts +++ b/frontend/src/lib/logger.ts @@ -1,8 +1,5 @@ import { createConsola } from 'consola/browser'; -const isProduction = false; - export const logger = createConsola({ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- replaced by Rollup at build time - level: isProduction ? 1 : 4, + level: import.meta.env.DEV ? 4 : 1, }); diff --git a/frontend/src/lib/notifications/__tests__/stream.test.ts b/frontend/src/lib/notifications/__tests__/stream.test.ts index 1d54d6225..8e2d88812 100644 --- a/frontend/src/lib/notifications/__tests__/stream.test.ts +++ b/frontend/src/lib/notifications/__tests__/stream.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import type { NotificationResponse } from '$lib/api'; -import { notificationStreamApiV1EventsNotificationsStreamGet } from '$lib/api'; -import { notificationStream } from '$lib/notifications/stream.svelte'; +import type { NotificationResponse } from '#lib/api/index.ts'; +import { notificationStreamApiV1EventsNotificationsStreamGet } from '#lib/api/index.ts'; +import { notificationStream } from '#lib/notifications/stream.svelte.ts'; type NotificationCallback = (data: NotificationResponse) => void; diff --git a/frontend/src/lib/notifications/stream.svelte.ts b/frontend/src/lib/notifications/stream.svelte.ts index ad4da380a..d868358b3 100644 --- a/frontend/src/lib/notifications/stream.svelte.ts +++ b/frontend/src/lib/notifications/stream.svelte.ts @@ -1,5 +1,5 @@ -import { notificationStreamApiV1EventsNotificationsStreamGet, type NotificationResponse } from '$lib/api'; -import { logger } from '$lib/logger'; +import { notificationStreamApiV1EventsNotificationsStreamGet, type NotificationResponse } from '#lib/api/index.ts'; +import { logger } from '#lib/logger.ts'; const log = logger.withTag('NotificationStream'); diff --git a/frontend/src/lib/user-settings.ts b/frontend/src/lib/user-settings.ts index 0ea01eb31..ab04679ca 100644 --- a/frontend/src/lib/user-settings.ts +++ b/frontend/src/lib/user-settings.ts @@ -1,12 +1,12 @@ -import { authStore } from '$stores/auth.svelte'; -import { setTheme } from '$stores/theme.svelte'; -import { setUserSettings } from '$stores/userSettings.svelte'; +import { authStore } from '#stores/auth.svelte.ts'; +import { setTheme } from '#stores/theme.svelte.ts'; +import { setUserSettings } from '#stores/userSettings.svelte.ts'; import { getUserSettingsApiV1UserSettingsGet, updateUserSettingsApiV1UserSettingsPut, type UserSettings, type UserSettingsUpdate, -} from '$lib/api'; +} from '#lib/api/index.ts'; export async function loadUserSettings(): Promise { const { data, error } = await getUserSettingsApiV1UserSettingsGet({}); diff --git a/frontend/src/lib/validation.ts b/frontend/src/lib/validation.ts index 70f64f3b6..e91710f82 100644 --- a/frontend/src/lib/validation.ts +++ b/frontend/src/lib/validation.ts @@ -1,5 +1,5 @@ -import type { z } from 'zod'; -import { formatEnumLabel } from '$lib/formatters'; +import type { core } from 'zod/mini'; +import { formatEnumLabel } from '#lib/formatters.ts'; /** * The first problem in a failed parse of a generated Zod schema, as one readable line. @@ -7,7 +7,7 @@ import { formatEnumLabel } from '$lib/formatters'; * @param error - The error from `safeParse` * @returns A sentence such as "Password must be at least 8 characters" */ -export function firstIssue(error: z.ZodError): string { +export function firstIssue(error: core.$ZodError): string { const issue = error.issues[0]; if (!issue) return 'Invalid input'; const last = issue.path[issue.path.length - 1]; @@ -23,6 +23,8 @@ export function firstIssue(error: z.ZodError): string { : `${field} must be at most ${issue.maximum}`; case 'invalid_format': return `${field} must be a valid ${issue.format}`; + case 'invalid_type': + return `${field} must be a ${issue.expected}`; default: return `${field}: ${issue.message}`; } diff --git a/frontend/src/main.ts b/frontend/src/main.ts deleted file mode 100644 index 8d2b94f38..000000000 --- a/frontend/src/main.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { mount } from 'svelte'; -import App from './App.svelte'; -import ErrorDisplay from '$components/ErrorDisplay.svelte'; -import { appError } from '$stores/errorStore.svelte'; -import { initializeApiInterceptors } from '$lib/api-interceptors'; -import { logger } from '$lib/logger'; -import './app.css'; - -const log = logger.withTag('Global'); - -initializeApiInterceptors(); - -// Global error handlers to catch unhandled errors -window.onerror = (message, source, lineno, colno, error) => { - log.error('[Global Error]', { message, source, lineno, colno, error }); - appError.setError(error ?? new Error(typeof message === 'string' ? message : 'Unknown error'), 'Unexpected Error'); - return true; // Prevent default browser error handling -}; - -window.onunhandledrejection = (event) => { - log.error('[Unhandled Promise Rejection]', event.reason); - const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason)); - appError.setError(error, 'Unexpected Error'); - event.preventDefault(); -}; - -// Mount the app with error handling -let app; -try { - app = mount(App, { - target: document.body, - }); -} catch (error) { - log.error('[Mount Error]', error); - // If App fails to mount, show error display directly - app = mount(ErrorDisplay, { - target: document.body, - props: { - error: error instanceof Error ? error : new Error(String(error)), - title: 'Failed to Load Application', - }, - }); -} - -export default app; diff --git a/frontend/src/routes/(auth)/+layout.ts b/frontend/src/routes/(auth)/+layout.ts new file mode 100644 index 000000000..1ac7c76a2 --- /dev/null +++ b/frontend/src/routes/(auth)/+layout.ts @@ -0,0 +1,6 @@ +import { requireAuth } from '#lib/auth-guard.ts'; +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async ({ url }) => { + await requireAuth(url); +}; diff --git a/frontend/src/routes/Editor.svelte b/frontend/src/routes/(auth)/editor/+page.svelte similarity index 95% rename from frontend/src/routes/Editor.svelte rename to frontend/src/routes/(auth)/editor/+page.svelte index eb75a18be..a1e027b27 100644 --- a/frontend/src/routes/Editor.svelte +++ b/frontend/src/routes/(auth)/editor/+page.svelte @@ -1,7 +1,7 @@ + + Code Editor - Integr8sCode + + +
diff --git a/frontend/src/routes/Notifications.svelte b/frontend/src/routes/(auth)/notifications/+page.svelte similarity index 98% rename from frontend/src/routes/Notifications.svelte rename to frontend/src/routes/(auth)/notifications/+page.svelte index d26fb6b2e..f027a74e6 100644 --- a/frontend/src/routes/Notifications.svelte +++ b/frontend/src/routes/(auth)/notifications/+page.svelte @@ -1,12 +1,12 @@ + + Settings - Integr8sCode + + + { if (!(e.target as Element)?.closest('.dropdown-container')) { diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte new file mode 100644 index 000000000..c12ea02a4 --- /dev/null +++ b/frontend/src/routes/+error.svelte @@ -0,0 +1,13 @@ + + + + {page.status === 404 ? 'Page not found' : 'Error'} - Integr8sCode + + + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 000000000..f89c17f25 --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,43 @@ + + +
+
+ +
+ {#if !authInitialized} +
+ +
+ {:else} + {@render children()} + {/if} +
+
+
diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts new file mode 100644 index 000000000..1836e04bc --- /dev/null +++ b/frontend/src/routes/+layout.ts @@ -0,0 +1,2 @@ +// A client-rendered SPA: adapter-static serves every route through the 200.html fallback +export const ssr = false; diff --git a/frontend/src/routes/Home.svelte b/frontend/src/routes/+page.svelte similarity index 95% rename from frontend/src/routes/Home.svelte rename to frontend/src/routes/+page.svelte index 92613a04c..688023e3a 100644 --- a/frontend/src/routes/Home.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,7 +1,5 @@ + + Home - Integr8sCode + + + {#if ready}
@@ -54,7 +59,6 @@
Start Coding Now diff --git a/frontend/src/routes/__tests__/Editor.test.ts b/frontend/src/routes/__tests__/Editor.test.ts index e43bcfbe4..fc2f48853 100644 --- a/frontend/src/routes/__tests__/Editor.test.ts +++ b/frontend/src/routes/__tests__/Editor.test.ts @@ -1,9 +1,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { user, mockApi } from '$test/test-utils'; +import { user, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import * as meta from '$utils/meta'; -import Editor from '$routes/Editor.svelte'; +import Editor from '#routes/(auth)/editor/+page.svelte'; function createMockLimits() { return { @@ -25,7 +24,7 @@ import { createSavedScriptApiV1ScriptsPost, updateSavedScriptApiV1ScriptsScriptIdPut, deleteSavedScriptApiV1ScriptsScriptIdDelete, -} from '$lib/api'; +} from '#lib/api/index.ts'; const mocks = vi.hoisted(() => ({ mockConfirm: vi.fn(), @@ -46,22 +45,22 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.mockAuthStore })); -vi.mock('$stores/userSettings.svelte', () => ({ +vi.mock('#stores/userSettings.svelte.ts', () => ({ userSettingsStore: { settings: null, editorSettings: { font_size: 14, tab_size: 4, use_tabs: false, word_wrap: true, show_line_numbers: true }, }, })); -vi.mock('$lib/editor', async () => ({ +vi.mock('#lib/editor/index.ts', async () => ({ createExecutionState: () => mocks.mockExecutionState, - newestVersion: (await import('$lib/editor/runtimes')).newestVersion, + newestVersion: (await import('#lib/editor/runtimes.ts')).newestVersion, })); -vi.mock('$components/editor', async () => { - const utils = await import('$test/test-utils'); +vi.mock('#components/editor/index.ts', async () => { + const utils = await import('#test/test-utils.ts'); const components = utils.createMockNamedComponents({ OutputPanel: '
Execution Output
', ResourceLimits: '
', @@ -76,13 +75,13 @@ vi.mock('$components/editor', async () => { head: '', }); components.CodeMirrorEditor = CodeMirrorEditor; - const { default: EditorToolbar } = await import('$components/editor/EditorToolbar.svelte'); + const { default: EditorToolbar } = await import('#components/editor/EditorToolbar.svelte'); components.EditorToolbar = EditorToolbar; - const { default: ScriptActions } = await import('$components/editor/ScriptActions.svelte'); + const { default: ScriptActions } = await import('#components/editor/ScriptActions.svelte'); components.ScriptActions = ScriptActions; - const { default: SavedScripts } = await import('$components/editor/SavedScripts.svelte'); + const { default: SavedScripts } = await import('#components/editor/SavedScripts.svelte'); components.SavedScripts = SavedScripts; - const { default: LanguageSelect } = await import('$components/editor/LanguageSelect.svelte'); + const { default: LanguageSelect } = await import('#components/editor/LanguageSelect.svelte'); components.LanguageSelect = LanguageSelect; return components; }); @@ -91,7 +90,6 @@ describe('Editor', () => { beforeEach(() => { vi.clearAllMocks(); localStorage.clear(); - vi.spyOn(meta, 'updateMetaTags'); vi.stubGlobal('confirm', mocks.mockConfirm); mocks.mockAuthStore.isAuthenticated = true; mocks.mockAuthStore.verifyAuth.mockResolvedValue(true); @@ -147,14 +145,9 @@ describe('Editor', () => { }); }); - it('calls updateMetaTags with editor meta', async () => { + it('sets the page title', async () => { await renderEditor(); - await waitFor(() => { - expect(meta.updateMetaTags).toHaveBeenCalledWith( - 'Code Editor', - expect.stringContaining('Online code editor'), - ); - }); + await waitFor(() => expect(document.title).toBe('Code Editor - Integr8sCode')); }); }); diff --git a/frontend/src/routes/__tests__/Home.test.ts b/frontend/src/routes/__tests__/Home.test.ts index 1e534b884..b83f1e337 100644 --- a/frontend/src/routes/__tests__/Home.test.ts +++ b/frontend/src/routes/__tests__/Home.test.ts @@ -1,12 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import * as meta from '$utils/meta'; -import Home from '$routes/Home.svelte'; +import Home from '#routes/+page.svelte'; describe('Home', () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(meta, 'updateMetaTags'); }); function renderHome() { @@ -50,10 +48,8 @@ describe('Home', () => { expect(screen.getByText(content)).toBeInTheDocument(); }); - it('calls updateMetaTags with home meta on mount', async () => { + it('sets the page title', async () => { await renderHome(); - await waitFor(() => { - expect(meta.updateMetaTags).toHaveBeenCalledWith('Home', expect.stringContaining('Integr8sCode')); - }); + await waitFor(() => expect(document.title).toBe('Home - Integr8sCode')); }); }); diff --git a/frontend/src/routes/__tests__/Login.test.ts b/frontend/src/routes/__tests__/Login.test.ts index f16e4ea80..eeaccc1f4 100644 --- a/frontend/src/routes/__tests__/Login.test.ts +++ b/frontend/src/routes/__tests__/Login.test.ts @@ -1,12 +1,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { user } from '$test/test-utils'; +import { user } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import * as router from '@mateothegreat/svelte5-router'; -import * as meta from '$utils/meta'; -import Login from '$routes/Login.svelte'; +import * as navigation from '$app/navigation'; +import Login from '#routes/login/+page.svelte'; -import { getErrorMessage } from '$lib/api-interceptors'; +import { getErrorMessage } from '#lib/api-interceptors.ts'; const mocks = vi.hoisted(() => ({ mockLogin: vi.fn(), @@ -20,9 +19,9 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.mockAuthStore })); -vi.mock('$lib/user-settings', () => ({ +vi.mock('#lib/user-settings.ts', () => ({ loadUserSettings: mocks.mockLoadUserSettings, })); @@ -31,7 +30,6 @@ describe('Login', () => { vi.clearAllMocks(); mocks.mockAuthStore.login = vi.fn().mockResolvedValue(true); mocks.mockLoadUserSettings.mockResolvedValue(undefined); - vi.spyOn(meta, 'updateMetaTags'); }); function renderLogin() { @@ -67,7 +65,7 @@ describe('Login', () => { }); expect(mocks.mockLoadUserSettings).toHaveBeenCalled(); expect(toast.success).toHaveBeenCalledWith('Login successful! Welcome back.'); - expect(router.goto).toHaveBeenCalledWith('/editor'); + expect(navigation.goto).toHaveBeenCalledWith('/editor'); }); it.each([ @@ -85,7 +83,7 @@ describe('Login', () => { await user.click(screen.getByRole('button', { name: /sign in/i })); await waitFor(() => { - expect(router.goto).toHaveBeenCalledWith(expectedNav); + expect(navigation.goto).toHaveBeenCalledWith(expectedNav); }); expect(sessionStorage.getItem('redirectAfterLogin')).toBeNull(); }); @@ -130,13 +128,8 @@ describe('Login', () => { }); }); - it('calls updateMetaTags on mount', async () => { + it('sets the page title', async () => { await renderLogin(); - await waitFor(() => { - expect(meta.updateMetaTags).toHaveBeenCalledWith( - 'Login', - expect.stringContaining('Sign in to Integr8sCode'), - ); - }); + await waitFor(() => expect(document.title).toBe('Login - Integr8sCode')); }); }); diff --git a/frontend/src/routes/__tests__/Notifications.test.ts b/frontend/src/routes/__tests__/Notifications.test.ts index 1ea43af24..0731388cc 100644 --- a/frontend/src/routes/__tests__/Notifications.test.ts +++ b/frontend/src/routes/__tests__/Notifications.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { createMockNotification, createMockNotifications, user } from '$test/test-utils'; +import { createMockNotification, createMockNotifications, user } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import Notifications from '$routes/Notifications.svelte'; +import Notifications from '#routes/(auth)/notifications/+page.svelte'; const mocks = vi.hoisted(() => ({ mockNotificationStore: { @@ -17,7 +17,7 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/notificationStore.svelte', () => ({ notificationStore: mocks.mockNotificationStore })); +vi.mock('#stores/notificationStore.svelte.ts', () => ({ notificationStore: mocks.mockNotificationStore })); describe('Notifications', () => { beforeEach(() => { diff --git a/frontend/src/routes/__tests__/Privacy.test.ts b/frontend/src/routes/__tests__/Privacy.test.ts index 35fc4bc51..9283bc915 100644 --- a/frontend/src/routes/__tests__/Privacy.test.ts +++ b/frontend/src/routes/__tests__/Privacy.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; -import Privacy from '$routes/Privacy.svelte'; +import Privacy from '#routes/privacy/+page.svelte'; const mocks = vi.hoisted(() => ({ scrollTo: vi.fn(), diff --git a/frontend/src/routes/__tests__/Register.test.ts b/frontend/src/routes/__tests__/Register.test.ts index 13e7cfa99..425c4544d 100644 --- a/frontend/src/routes/__tests__/Register.test.ts +++ b/frontend/src/routes/__tests__/Register.test.ts @@ -1,19 +1,17 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { user, mockApi } from '$test/test-utils'; +import { user, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import * as router from '@mateothegreat/svelte5-router'; -import * as meta from '$utils/meta'; -import Register from '$routes/Register.svelte'; +import * as navigation from '$app/navigation'; +import Register from '#routes/register/+page.svelte'; -import { registerApiV1AuthRegisterPost } from '$lib/api'; -import { getErrorMessage } from '$lib/api-interceptors'; +import { registerApiV1AuthRegisterPost } from '#lib/api/index.ts'; +import { getErrorMessage } from '#lib/api-interceptors.ts'; describe('Register', () => { beforeEach(() => { vi.clearAllMocks(); mockApi(registerApiV1AuthRegisterPost).ok({}); - vi.spyOn(meta, 'updateMetaTags'); }); function renderRegister() { @@ -78,7 +76,7 @@ describe('Register', () => { }); }); expect(toast.success).toHaveBeenCalledWith('Registration successful! Please log in.'); - expect(router.goto).toHaveBeenCalledWith('/login'); + expect(navigation.goto).toHaveBeenCalledWith('/login'); }); it('shows error in DOM on API error (no duplicate toast)', async () => { @@ -125,13 +123,8 @@ describe('Register', () => { }); }); - it('calls updateMetaTags on mount', async () => { + it('sets the page title', async () => { await renderRegister(); - await waitFor(() => { - expect(meta.updateMetaTags).toHaveBeenCalledWith( - 'Register', - expect.stringContaining('Create a free Integr8sCode account'), - ); - }); + await waitFor(() => expect(document.title).toBe('Register - Integr8sCode')); }); }); diff --git a/frontend/src/routes/__tests__/Settings.test.ts b/frontend/src/routes/__tests__/Settings.test.ts index 0bbecefe1..f0784966c 100644 --- a/frontend/src/routes/__tests__/Settings.test.ts +++ b/frontend/src/routes/__tests__/Settings.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { user, mockApi } from '$test/test-utils'; +import { user, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import Settings from '$routes/Settings.svelte'; +import Settings from '#routes/(auth)/settings/+page.svelte'; function createMockSettings() { return { @@ -35,7 +35,7 @@ import { updateUserSettingsApiV1UserSettingsPut, restoreSettingsApiV1UserSettingsRestorePost, getSettingsHistoryApiV1UserSettingsHistoryGet, -} from '$lib/api'; +} from '#lib/api/index.ts'; const mocks = vi.hoisted(() => ({ mockSetTheme: vi.fn(), @@ -49,9 +49,9 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); -vi.mock('$stores/theme.svelte', () => ({ setTheme: (...args: unknown[]) => mocks.mockSetTheme(...args) })); -vi.mock('$stores/userSettings.svelte', () => ({ +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.mockAuthStore })); +vi.mock('#stores/theme.svelte.ts', () => ({ setTheme: (...args: unknown[]) => mocks.mockSetTheme(...args) })); +vi.mock('#stores/userSettings.svelte.ts', () => ({ setUserSettings: (...args: unknown[]) => mocks.mockSetUserSettings(...args), userSettingsStore: { settings: null, editorSettings: {} }, })); diff --git a/frontend/src/routes/__tests__/error.test.ts b/frontend/src/routes/__tests__/error.test.ts new file mode 100644 index 000000000..01cf65452 --- /dev/null +++ b/frontend/src/routes/__tests__/error.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import ErrorPage from '#routes/+error.svelte'; + +const mocks = vi.hoisted(() => ({ + page: { status: 404, error: { message: 'Not Found' } as { message: string } | null }, +})); + +vi.mock('$app/state', () => ({ page: mocks.page })); + +describe('+error', () => { + it.each([ + [404, 'Page not found'], + [500, 'Something went wrong'], + ])('shows a %i as "%s"', (status, heading) => { + mocks.page.status = status; + render(ErrorPage); + expect(screen.getByRole('heading', { name: heading })).toBeInTheDocument(); + expect(document.title).toBe(`${status === 404 ? 'Page not found' : 'Error'} - Integr8sCode`); + }); +}); diff --git a/frontend/src/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte new file mode 100644 index 000000000..1a2521100 --- /dev/null +++ b/frontend/src/routes/admin/+layout.svelte @@ -0,0 +1,54 @@ + + +
+ +
+
+
+ +

Admin Panel

+
+ +
+ + {#if authStore.username} +
+

Logged in as:

+

{authStore.username}

+
+ {/if} +
+
+ + +
+ {@render children()} +
+
diff --git a/frontend/src/routes/admin/+layout.ts b/frontend/src/routes/admin/+layout.ts new file mode 100644 index 000000000..0b439cbdb --- /dev/null +++ b/frontend/src/routes/admin/+layout.ts @@ -0,0 +1,6 @@ +import { requireAdmin } from '#lib/auth-guard.ts'; +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async ({ url }) => { + await requireAdmin(url); +}; diff --git a/frontend/src/routes/admin/+page.ts b/frontend/src/routes/admin/+page.ts new file mode 100644 index 000000000..c7349a123 --- /dev/null +++ b/frontend/src/routes/admin/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export function load(): never { + redirect(307, '/admin/events'); +} diff --git a/frontend/src/routes/admin/AdminEvents.svelte b/frontend/src/routes/admin/AdminEvents.svelte deleted file mode 100644 index 3b4687640..000000000 --- a/frontend/src/routes/admin/AdminEvents.svelte +++ /dev/null @@ -1,256 +0,0 @@ - - - { - if (showExportMenu && !(e.target as Element)?.closest('.export-dropdown')) { - showExportMenu = false; - } - }} -/> - - -
- -
-

Event Browser

- -
- - - -
- - - {#if showExportMenu} -
- - -
- {/if} -
- - -
-
- - (store.activeReplaySession = null)} /> - - - - - {#if !showFilters && hasActiveFilters(store.filters)} -
- Active filters: - {#each getActiveFilterSummary(store.filters) as filter} - - {filter} - - {/each} - -
- {/if} - - {#if showFilters} - store.applyFilters()} - onClear={() => store.clearFilters()} - /> - {/if} - - -
-
-
-

Events

-
- - store.deleteEvent(id)} - onViewUser={handleUserOverview} - /> - - { - store.pagination.currentPage = page; - store.loadEvents(); - }} - onPageSizeChange={() => { - store.pagination.currentPage = 1; - store.loadEvents(); - }} - itemName="events" - /> -
-
-
-
- - -{#if selectedEvent} - (selectedEvent = null)} - onReplay={(id) => handleReplay(id, true)} - onViewRelated={loadEventDetail} - /> -{/if} - -{#if showReplayPreview && store.replayPreview} - { - store.replayPreview = null; - }} - onConfirm={handleReplay} - /> -{/if} - -{#if showUserOverview} - (showUserOverview = false)} - /> -{/if} diff --git a/frontend/src/routes/admin/AdminExecutions.svelte b/frontend/src/routes/admin/AdminExecutions.svelte deleted file mode 100644 index 3128f754f..000000000 --- a/frontend/src/routes/admin/AdminExecutions.svelte +++ /dev/null @@ -1,224 +0,0 @@ - - - -
-
-

- Execution Management -

- -
- - - {#if store.queueStatus} -
-
-
Queue Depth
-
- {store.queueStatus.queue_depth} -
-
-
-
Active
-
- {store.queueStatus.active_count} / {store.queueStatus.max_concurrent} -
-
-
-
By Priority
-
- {#each Object.entries(store.queueStatus.by_priority) as [priority, count]} - {priority}: {count} - {:else} - Empty - {/each} -
-
-
- {/if} - - -
-
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
-

- Executions ({store.total}) -

- - {#if store.loading && store.executions.length === 0} -
- {:else if store.executions.length === 0} -

No executions found

- {:else} -
- - - - - - - - - - - - - {#each store.executions as exec} - - - - - - - - - {/each} - -
IDUserLanguageStatusPriorityCreated
{truncate(exec.execution_id, 12)}{exec.user_id ? truncate(exec.user_id, 12) : '-'}{exec.lang} {exec.lang_version} - {exec.status} - - - {formatTimestamp(exec.created_at)}
-
- {/if} - - {#if totalPages > 1} -
- - store.pagination.handlePageChange(page, () => store.loadExecutions())} - onPageSizeChange={() => { - store.pagination.currentPage = 1; - store.loadExecutions(); - }} - pageSizeOptions={[10, 20, 50, 100]} - itemName="executions" - /> -
- {/if} -
-
-
-
diff --git a/frontend/src/routes/admin/AdminLayout.svelte b/frontend/src/routes/admin/AdminLayout.svelte deleted file mode 100644 index 0fe8ae3d4..000000000 --- a/frontend/src/routes/admin/AdminLayout.svelte +++ /dev/null @@ -1,81 +0,0 @@ - - -{#if loading} -
-
- -

Verifying authentication...

-
-
-{:else} -
- -
-
-
- -

Admin Panel

-
- - - - {#if user} -
-

Logged in as:

-

{user.username}

-
- {/if} -
-
- - -
- {@render children?.()} -
-
-{/if} diff --git a/frontend/src/routes/admin/AdminSagas.svelte b/frontend/src/routes/admin/AdminSagas.svelte deleted file mode 100644 index e4abd81a2..000000000 --- a/frontend/src/routes/admin/AdminSagas.svelte +++ /dev/null @@ -1,122 +0,0 @@ - - - -
-
-

Saga Management

-

Monitor and debug distributed transactions

-
- - - -
- store.loadSagas()} - /> -
- - store.loadSagas()} - onClear={() => store.clearFilters()} - /> - - store.loadExecutionSagas(id)} - /> - - {#if store.totalItems > 0} -
- {#if store.hasClientFilters && store.sagas.length < store.serverReturnedCount} -

- Showing {store.sagas.length} of {store.serverReturnedCount} on this page (filtered locally) -

- {/if} - -
- {/if} -
-
- -{#if showDetailModal && selectedSaga} - (showDetailModal = false)} - onViewExecution={handleViewExecution} - /> -{/if} diff --git a/frontend/src/routes/admin/AdminSettings.svelte b/frontend/src/routes/admin/AdminSettings.svelte deleted file mode 100644 index 515b05731..000000000 --- a/frontend/src/routes/admin/AdminSettings.svelte +++ /dev/null @@ -1,329 +0,0 @@ - - - -
-
-

System Settings

-
- -
-
-

Configuration

- - {#if loading} -
- -
- {:else} -
- -
-

- Execution Limits -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-

- Security Settings -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-

- Monitoring Settings -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
- - -
-
- {/if} -
-
-
-
diff --git a/frontend/src/routes/admin/__tests__/AdminEvents.test.ts b/frontend/src/routes/admin/__tests__/AdminEvents.test.ts index ae3305ee8..823b65a55 100644 --- a/frontend/src/routes/admin/__tests__/AdminEvents.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminEvents.test.ts @@ -11,7 +11,7 @@ import { user, selectOption, mockApi, -} from '$test/test-utils'; +} from '#test/test-utils.ts'; import { browseEventsApiV1AdminEventsBrowsePost, getEventStatsApiV1AdminEventsStatsGet, @@ -20,7 +20,7 @@ import { replayEventsApiV1AdminEventsReplayPost, deleteEventApiV1AdminEventsEventIdDelete, getUserOverviewApiV1AdminUsersUserIdOverviewGet, -} from '$lib/api'; +} from '#lib/api/index.ts'; import { toast } from 'svelte-sonner'; const mocks = vi.hoisted(() => ({ @@ -28,12 +28,7 @@ const mocks = vi.hoisted(() => ({ windowConfirm: vi.fn(), })); -vi.mock('$routes/admin/AdminLayout.svelte', async () => { - const { default: MockLayout } = await import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte'); - return { default: MockLayout }; -}); - -import AdminEvents from '$routes/admin/AdminEvents.svelte'; +import AdminEvents from '#routes/admin/events/+page.svelte'; async function renderWithEvents(events = createMockEvents(5), stats = createMockStats()) { mockApi(browseEventsApiV1AdminEventsBrowsePost).ok({ events, total: events.length, skip: 0, limit: 10 }); diff --git a/frontend/src/routes/admin/__tests__/AdminExecutions.test.ts b/frontend/src/routes/admin/__tests__/AdminExecutions.test.ts index 10599e5c2..172d2da26 100644 --- a/frontend/src/routes/admin/__tests__/AdminExecutions.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminExecutions.test.ts @@ -7,20 +7,15 @@ import { createMockExecutions, createMockQueueStatus, mockApi, -} from '$test/test-utils'; +} from '#test/test-utils.ts'; import { listExecutionsApiV1AdminExecutionsGet, updatePriorityApiV1AdminExecutionsExecutionIdPriorityPut, getQueueStatusApiV1AdminExecutionsQueueGet, -} from '$lib/api'; +} from '#lib/api/index.ts'; import { toast } from 'svelte-sonner'; -vi.mock('$routes/admin/AdminLayout.svelte', async () => { - const { default: MockLayout } = await import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte'); - return { default: MockLayout }; -}); - -import AdminExecutions from '$routes/admin/AdminExecutions.svelte'; +import AdminExecutions from '#routes/admin/executions/+page.svelte'; async function renderWithExecutions(executions = createMockExecutions(5), queueStatus = createMockQueueStatus()) { mockApi(listExecutionsApiV1AdminExecutionsGet).ok({ diff --git a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts index f3d311ad9..1bfa91f04 100644 --- a/frontend/src/routes/admin/__tests__/AdminLayout.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminLayout.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { toast } from 'svelte-sonner'; -import * as router from '@mateothegreat/svelte5-router'; -import AdminLayout from '$routes/admin/AdminLayout.svelte'; +import { createRawSnippet } from 'svelte'; +import AdminLayout from '#routes/admin/+layout.svelte'; const mocks = vi.hoisted(() => ({ + page: { url: new URL('https://localhost/admin/events') }, mockAuthStore: { isAuthenticated: true, username: 'adminuser', @@ -16,7 +16,8 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.mockAuthStore })); +vi.mock('$app/state', () => ({ page: mocks.page })); describe('AdminLayout', () => { beforeEach(() => { @@ -26,16 +27,14 @@ describe('AdminLayout', () => { }); function renderLayout(path = '/admin/events') { - return render(AdminLayout, { props: { path } }); + mocks.page.url = new URL(`https://localhost${path}`); + const children = createRawSnippet(() => ({ render: () => '

page content

' })); + return render(AdminLayout, { props: { children } }); } - it('redirects non-admin users with error toast', async () => { - mocks.mockAuthStore.userRole = 'user'; + it('renders the page inside the layout', async () => { await renderLayout(); - await waitFor(() => { - expect(toast.error).toHaveBeenCalledWith('Admin access required'); - }); - expect(router.goto).toHaveBeenCalledWith('/'); + await waitFor(() => expect(screen.getByText('page content')).toBeInTheDocument()); }); it('renders sidebar with "Admin Panel" heading for admin users', async () => { diff --git a/frontend/src/routes/admin/__tests__/AdminSagas.test.ts b/frontend/src/routes/admin/__tests__/AdminSagas.test.ts index fa6fe39b1..67bd67d53 100644 --- a/frontend/src/routes/admin/__tests__/AdminSagas.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminSagas.test.ts @@ -1,15 +1,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/svelte'; +import { render, screen, waitFor, within } from '@testing-library/svelte'; import { tick } from 'svelte'; -import { user, selectOption, createMockSaga, createMockSagas, mockApi } from '$test/test-utils'; -import { listSagasApiV1SagasGet, getSagaStatusApiV1SagasSagaIdGet } from '$lib/api'; +import { user, selectOption, createMockSaga, createMockSagas, mockApi } from '#test/test-utils.ts'; +import { listSagasApiV1SagasGet, getSagaStatusApiV1SagasSagaIdGet } from '#lib/api/index.ts'; -vi.mock('$routes/admin/AdminLayout.svelte', async () => { - const { default: MockLayout } = await import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte'); - return { default: MockLayout }; -}); - -import AdminSagas from '$routes/admin/AdminSagas.svelte'; +import { SAGA_STATES } from '#lib/admin/sagas/index.ts'; +import AdminSagas from '#routes/admin/sagas/+page.svelte'; async function renderWithSagas(sagas = createMockSagas(5)) { mockApi(listSagasApiV1SagasGet).ok({ sagas, total: sagas.length, skip: 0, limit: 10, has_more: false }); @@ -86,8 +82,11 @@ describe('AdminSagas', () => { createMockSaga({ saga_id: 's3', state: 'failed' }), ]; await renderWithSagas(sagas); - const statsSection = screen.getByTestId('admin-layout'); - expect(statsSection).toBeInTheDocument(); + const completedCard = screen + .getAllByText(SAGA_STATES.completed.label) + .map((el) => el.closest('.card')) + .find((card): card is HTMLElement => card instanceof HTMLElement); + expect(within(completedCard!).getByText('2')).toBeInTheDocument(); }); }); diff --git a/frontend/src/routes/admin/__tests__/AdminSettings.test.ts b/frontend/src/routes/admin/__tests__/AdminSettings.test.ts index e551e0179..681e460ec 100644 --- a/frontend/src/routes/admin/__tests__/AdminSettings.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminSettings.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; -import { user, mockApi } from '$test/test-utils'; +import { user, mockApi } from '#test/test-utils.ts'; import { toast } from 'svelte-sonner'; -import AdminSettings from '$routes/admin/AdminSettings.svelte'; +import AdminSettings from '#routes/admin/settings/+page.svelte'; function createMockSystemSettings() { return { @@ -26,7 +26,7 @@ import { getSystemSettingsApiV1AdminSettingsGet, updateSystemSettingsApiV1AdminSettingsPut, resetSystemSettingsApiV1AdminSettingsResetPost, -} from '$lib/api'; +} from '#lib/api/index.ts'; const mocks = vi.hoisted(() => ({ mockConfirm: vi.fn(), @@ -38,9 +38,7 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock('$stores/auth.svelte', () => ({ authStore: mocks.mockAuthStore })); - -vi.mock('$routes/admin/AdminLayout.svelte', () => import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte')); +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: mocks.mockAuthStore })); describe('AdminSettings', () => { beforeEach(() => { diff --git a/frontend/src/routes/admin/__tests__/AdminUsers.test.ts b/frontend/src/routes/admin/__tests__/AdminUsers.test.ts index c533730b2..1c5274427 100644 --- a/frontend/src/routes/admin/__tests__/AdminUsers.test.ts +++ b/frontend/src/routes/admin/__tests__/AdminUsers.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, waitFor, within } from '@testing-library/svelte'; -import { user, selectOption, createMockUser, createMockUsers, mockApi } from '$test/test-utils'; +import { user, selectOption, createMockUser, createMockUsers, mockApi } from '#test/test-utils.ts'; import { listUsersApiV1AdminUsersGet, createUserApiV1AdminUsersPost, @@ -10,21 +10,16 @@ import { updateUserRateLimitsApiV1AdminRateLimitsUserIdPut, resetUserRateLimitsApiV1AdminRateLimitsUserIdResetPost, getDefaultRateLimitRulesApiV1AdminRateLimitsDefaultsGet, -} from '$lib/api'; +} from '#lib/api/index.ts'; import { toast } from 'svelte-sonner'; -vi.mock('$lib/formatters', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('#lib/formatters.ts', async (importOriginal) => ({ + ...(await importOriginal()), formatTimestamp: (ts: string) => (ts ? `formatted:${ts}` : 'N/A'), })); // Simple mock for AdminLayout that just renders children -vi.mock('$routes/admin/AdminLayout.svelte', async () => { - const { default: MockLayout } = await import('$routes/admin/__tests__/mocks/MockAdminLayout.svelte'); - return { default: MockLayout }; -}); - -import AdminUsers from '$routes/admin/AdminUsers.svelte'; +import AdminUsers from '#routes/admin/users/+page.svelte'; async function renderWithUsers(users = createMockUsers(3)) { mockApi(listUsersApiV1AdminUsersGet).ok({ users, total: users.length, offset: 0, limit: 20 }); diff --git a/frontend/src/routes/admin/__tests__/mocks/MockAdminLayout.svelte b/frontend/src/routes/admin/__tests__/mocks/MockAdminLayout.svelte deleted file mode 100644 index a37d025bf..000000000 --- a/frontend/src/routes/admin/__tests__/mocks/MockAdminLayout.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
- {@render children()} -
diff --git a/frontend/src/routes/admin/events/+page.svelte b/frontend/src/routes/admin/events/+page.svelte new file mode 100644 index 000000000..9d31a7eb9 --- /dev/null +++ b/frontend/src/routes/admin/events/+page.svelte @@ -0,0 +1,253 @@ + + + { + if (showExportMenu && !(e.target as Element)?.closest('.export-dropdown')) { + showExportMenu = false; + } + }} +/> + +
+ +
+

Event Browser

+ +
+ + + +
+ + + {#if showExportMenu} +
+ + +
+ {/if} +
+ + +
+
+ + (store.activeReplaySession = null)} /> + + + + + {#if !showFilters && hasActiveFilters(store.filters)} +
+ Active filters: + {#each getActiveFilterSummary(store.filters) as filter} + + {filter} + + {/each} + +
+ {/if} + + {#if showFilters} + store.applyFilters()} + onClear={() => store.clearFilters()} + /> + {/if} + + +
+
+
+

Events

+
+ + store.deleteEvent(id)} + onViewUser={handleUserOverview} + /> + + { + store.pagination.currentPage = page; + store.loadEvents(); + }} + onPageSizeChange={() => { + store.pagination.currentPage = 1; + store.loadEvents(); + }} + itemName="events" + /> +
+
+
+ + +{#if selectedEvent} + (selectedEvent = null)} + onReplay={(id) => handleReplay(id, true)} + onViewRelated={loadEventDetail} + /> +{/if} + +{#if showReplayPreview && store.replayPreview} + { + store.replayPreview = null; + }} + onConfirm={handleReplay} + /> +{/if} + +{#if showUserOverview} + (showUserOverview = false)} + /> +{/if} diff --git a/frontend/src/routes/admin/executions/+page.svelte b/frontend/src/routes/admin/executions/+page.svelte new file mode 100644 index 000000000..55274180d --- /dev/null +++ b/frontend/src/routes/admin/executions/+page.svelte @@ -0,0 +1,213 @@ + + +
+
+

Execution Management

+ +
+ + + {#if store.queueStatus} +
+
+
Queue Depth
+
+ {store.queueStatus.queue_depth} +
+
+
+
Active
+
+ {store.queueStatus.active_count} / {store.queueStatus.max_concurrent} +
+
+
+
By Priority
+
+ {#each Object.entries(store.queueStatus.by_priority) as [priority, count]} + {priority}: {count} + {:else} + Empty + {/each} +
+
+
+ {/if} + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+

+ Executions ({store.total}) +

+ + {#if store.loading && store.executions.length === 0} +
+ {:else if store.executions.length === 0} +

No executions found

+ {:else} +
+ + + + + + + + + + + + + {#each store.executions as exec} + + + + + + + + + {/each} + +
IDUserLanguageStatusPriorityCreated
{truncate(exec.execution_id, 12)}{exec.user_id ? truncate(exec.user_id, 12) : '-'}{exec.lang} {exec.lang_version} + {exec.status} + + + {formatTimestamp(exec.created_at)}
+
+ {/if} + + {#if totalPages > 1} +
+ store.pagination.handlePageChange(page, () => store.loadExecutions())} + onPageSizeChange={() => { + store.pagination.currentPage = 1; + store.loadExecutions(); + }} + pageSizeOptions={[10, 20, 50, 100]} + itemName="executions" + /> +
+ {/if} +
+
+
diff --git a/frontend/src/routes/admin/sagas/+page.svelte b/frontend/src/routes/admin/sagas/+page.svelte new file mode 100644 index 000000000..425659748 --- /dev/null +++ b/frontend/src/routes/admin/sagas/+page.svelte @@ -0,0 +1,119 @@ + + +
+
+

Saga Management

+

Monitor and debug distributed transactions

+
+ + + +
+ store.loadSagas()} + /> +
+ + store.loadSagas()} + onClear={() => store.clearFilters()} + /> + + store.loadExecutionSagas(id)} + /> + + {#if store.totalItems > 0} +
+ {#if store.hasClientFilters && store.sagas.length < store.serverReturnedCount} +

+ Showing {store.sagas.length} of {store.serverReturnedCount} on this page (filtered locally) +

+ {/if} + +
+ {/if} +
+ +{#if showDetailModal && selectedSaga} + (showDetailModal = false)} + onViewExecution={handleViewExecution} + /> +{/if} diff --git a/frontend/src/routes/admin/settings/+page.svelte b/frontend/src/routes/admin/settings/+page.svelte new file mode 100644 index 000000000..9691120ed --- /dev/null +++ b/frontend/src/routes/admin/settings/+page.svelte @@ -0,0 +1,326 @@ + + +
+
+

System Settings

+
+ +
+
+

Configuration

+ + {#if loading} +
+ +
+ {:else} +
+ +
+

+ Execution Limits +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+ Security Settings +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

+ Monitoring Settings +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+ {/if} +
+
+
diff --git a/frontend/src/routes/admin/AdminUsers.svelte b/frontend/src/routes/admin/users/+page.svelte similarity index 78% rename from frontend/src/routes/admin/AdminUsers.svelte rename to frontend/src/routes/admin/users/+page.svelte index 88ea525d8..72f29eaca 100644 --- a/frontend/src/routes/admin/AdminUsers.svelte +++ b/frontend/src/routes/admin/users/+page.svelte @@ -1,4 +1,5 @@ - -
-
-

User Management

-
- - -
+
+
+

User Management

+
+ +
+
- + -
-
-

- Users ({filteredUsers.length}{filteredUsers.length !== users.length ? ` of ${users.length}` : ''}) -

+
+
+

+ Users ({filteredUsers.length}{filteredUsers.length !== users.length ? ` of ${users.length}` : ''}) +

- + - {#if totalPages > 1 || filteredUsers.length > 0} -
- -
- {/if} -
+ {#if totalPages > 1 || filteredUsers.length > 0} +
+ +
+ {/if}
- +
{#if showDeleteModal && userToDelete} - import { goto, route } from '@mateothegreat/svelte5-router'; - import { authStore } from '$stores/auth.svelte'; + import { goto } from '$app/navigation'; + import { authStore } from '#stores/auth.svelte.ts'; import { toast } from 'svelte-sonner'; import { fade, fly } from 'svelte/transition'; - import Spinner from '$components/Spinner.svelte'; + import Spinner from '#components/Spinner.svelte'; import { onMount } from 'svelte'; - import { updateMetaTags, pageMeta } from '$utils/meta'; - import { loadUserSettings } from '$lib/user-settings'; - import { getErrorMessage } from '$lib/api-interceptors'; - import { logger } from '$lib/logger'; + import { loadUserSettings } from '#lib/user-settings.ts'; + import { getErrorMessage } from '#lib/api-interceptors.ts'; + import { logger } from '#lib/logger.ts'; const log = logger.withTag('Login'); @@ -18,8 +17,6 @@ let error = $state(null); onMount(() => { - updateMetaTags(pageMeta.login.title, pageMeta.login.description); - // Check for authentication message from redirect const authMessage = sessionStorage.getItem('authMessage'); if (authMessage) { @@ -57,6 +54,14 @@ } + + Login - Integr8sCode + + +
@@ -64,7 +69,6 @@

Or create a new account diff --git a/frontend/src/routes/Privacy.svelte b/frontend/src/routes/privacy/+page.svelte similarity index 98% rename from frontend/src/routes/Privacy.svelte rename to frontend/src/routes/privacy/+page.svelte index 17e5450ad..4176668e2 100644 --- a/frontend/src/routes/Privacy.svelte +++ b/frontend/src/routes/privacy/+page.svelte @@ -1,6 +1,5 @@ + + Privacy Policy - Integr8sCode + + +

- import { goto, route } from '@mateothegreat/svelte5-router'; + import { goto } from '$app/navigation'; import { toast } from 'svelte-sonner'; import { fade, fly } from 'svelte/transition'; - import { registerApiV1AuthRegisterPost, zUserCreate } from '$lib/api'; - import { firstIssue } from '$lib/validation'; - import { getErrorMessage } from '$lib/api-interceptors'; - import { onMount } from 'svelte'; - import { updateMetaTags, pageMeta } from '$utils/meta'; - import Spinner from '$components/Spinner.svelte'; + import { registerApiV1AuthRegisterPost, zUserCreate } from '#lib/api/index.ts'; + import { firstIssue } from '#lib/validation.ts'; + import { getErrorMessage } from '#lib/api-interceptors.ts'; + import Spinner from '#components/Spinner.svelte'; let username = $state(''); let email = $state(''); @@ -16,10 +14,6 @@ let loading = $state(false); let error = $state(null); - onMount(() => { - updateMetaTags(pageMeta.register.title, pageMeta.register.description); - }); - async function handleRegister() { if (password !== confirmPassword) { error = 'Passwords do not match.'; @@ -49,6 +43,14 @@ } + + Register - Integr8sCode + + +
@@ -56,7 +58,6 @@

Or sign in to your existing account diff --git a/frontend/src/stores/__tests__/auth.test.ts b/frontend/src/stores/__tests__/auth.test.ts index dff2c462c..4580b9112 100644 --- a/frontend/src/stores/__tests__/auth.test.ts +++ b/frontend/src/stores/__tests__/auth.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { suppressConsoleError, suppressConsoleWarn } from '$test/test-utils'; +import { suppressConsoleError, suppressConsoleWarn } from '#test/test-utils.ts'; const mockLoginApi = vi.fn(); const mockLogoutApi = vi.fn(); const mockGetProfileApi = vi.fn(); -vi.mock('$lib/api', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('#lib/api/index.ts', async (importOriginal) => ({ + ...(await importOriginal()), loginApiV1AuthLoginPost: (...args: unknown[]) => mockLoginApi(...args), logoutApiV1AuthLogoutPost: (...args: unknown[]) => mockLogoutApi(...args), getCurrentUserProfileApiV1AuthMeGet: (...args: unknown[]) => mockGetProfileApi(...args), @@ -14,7 +14,7 @@ vi.mock('$lib/api', async (importOriginal) => ({ // Mock clearUserSettings (static import in auth.svelte.ts) const mockClearUserSettings = vi.fn(); -vi.mock('$stores/userSettings.svelte', () => ({ +vi.mock('#stores/userSettings.svelte.ts', () => ({ clearUserSettings: () => mockClearUserSettings(), setUserSettings: vi.fn(), userSettingsStore: { settings: null, editorSettings: {} }, @@ -22,7 +22,7 @@ vi.mock('$stores/userSettings.svelte', () => ({ // Mock loadUserSettings (dynamic import in auth.svelte.ts) const mockLoadUserSettings = vi.fn(); -vi.mock('$lib/user-settings', () => ({ +vi.mock('#lib/user-settings.ts', () => ({ loadUserSettings: () => mockLoadUserSettings(), })); @@ -70,17 +70,17 @@ describe('auth store', () => { describe('initial state', () => { it('has null authentication state initially', async () => { - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); expect(authStore.isAuthenticated).toBe(null); }); it('has null username initially', async () => { - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); expect(authStore.username).toBe(null); }); it('has null userRole initially', async () => { - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); expect(authStore.userRole).toBe(null); }); @@ -96,7 +96,7 @@ describe('auth store', () => { }; sessionStorageData['authState'] = JSON.stringify(authState); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); expect(authStore.isAuthenticated).toBe(true); expect(authStore.username).toBe('testuser'); @@ -120,7 +120,7 @@ describe('auth store', () => { error: null, }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.login('testuser', 'password123'); @@ -144,7 +144,7 @@ describe('auth store', () => { error: null, }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.login('testuser', 'password123'); expect(sessionStorage.setItem).toHaveBeenCalledWith('authState', expect.stringContaining('testuser')); @@ -156,7 +156,7 @@ describe('auth store', () => { error: { detail: 'Invalid credentials' }, }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await expect(authStore.login('baduser', 'badpass')).rejects.toBeDefined(); }); @@ -169,7 +169,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValue(UNAUTHORIZED); const restoreConsole = suppressConsoleWarn(); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.login('testuser', 'password123'); expect(result).toBe(false); @@ -185,7 +185,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValue(NETWORK_ERROR); const restoreConsole = suppressConsoleWarn(); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.login('testuser', 'password123'); expect(result).toBe(true); @@ -203,7 +203,7 @@ describe('auth store', () => { error: null, }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.login('testuser', 'mypassword'); expect(mockLoginApi).toHaveBeenCalledWith({ @@ -224,7 +224,7 @@ describe('auth store', () => { }); mockLogoutApi.mockResolvedValue({ data: {}, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.login('testuser', 'password'); expect(authStore.isAuthenticated).toBe(true); @@ -238,7 +238,7 @@ describe('auth store', () => { it('clears sessionStorage on logout', async () => { mockLogoutApi.mockResolvedValue({ data: {}, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.logout(); expect(sessionStorage.removeItem).toHaveBeenCalledWith('authState'); @@ -257,7 +257,7 @@ describe('auth store', () => { }); mockLogoutApi.mockRejectedValue(new Error('Network error')); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.login('testuser', 'password'); await authStore.logout(); @@ -271,7 +271,7 @@ describe('auth store', () => { it('returns true when verification succeeds', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.verifyAuth(true); expect(result).toBe(true); @@ -280,7 +280,7 @@ describe('auth store', () => { it('returns false and clears state on a 401', async () => { mockGetProfileApi.mockResolvedValue(UNAUTHORIZED); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.verifyAuth(true); expect(result).toBe(false); @@ -291,7 +291,7 @@ describe('auth store', () => { it('uses cache when not force refreshing', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.verifyAuth(true); expect(mockGetProfileApi).toHaveBeenCalledTimes(1); @@ -303,7 +303,7 @@ describe('auth store', () => { it('bypasses cache when force refreshing', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.verifyAuth(true); await authStore.verifyAuth(true); @@ -319,7 +319,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValueOnce({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const firstResult = await authStore.verifyAuth(true); expect(firstResult).toBe(true); @@ -339,7 +339,7 @@ describe('auth store', () => { const restoreConsole = suppressConsoleWarn(); mockGetProfileApi.mockResolvedValue(NETWORK_ERROR); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.verifyAuth(true); expect(result).toBe(false); @@ -353,7 +353,7 @@ describe('auth store', () => { it('returns false when no persisted auth and verification fails', async () => { mockGetProfileApi.mockResolvedValue(UNAUTHORIZED); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(false); @@ -362,7 +362,7 @@ describe('auth store', () => { it('returns true when no persisted auth but verification succeeds', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(true); @@ -372,7 +372,7 @@ describe('auth store', () => { it('only initializes once', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.initialize(); await authStore.initialize(); await authStore.initialize(); @@ -395,7 +395,7 @@ describe('auth store', () => { ), ); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const results = await Promise.all([authStore.initialize(), authStore.initialize(), authStore.initialize()]); expect(results).toEqual([true, true, true]); @@ -419,7 +419,7 @@ describe('auth store', () => { error: null, }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(true); @@ -441,7 +441,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValue(UNAUTHORIZED); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(false); @@ -465,7 +465,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValue(NETWORK_ERROR); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(true); @@ -491,7 +491,7 @@ describe('auth store', () => { mockGetProfileApi.mockResolvedValue(NETWORK_ERROR); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.initialize(); expect(result).toBe(false); @@ -505,7 +505,7 @@ describe('auth store', () => { it('returns immediately if already initialized', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.initialize(); const result = await authStore.waitForInit(); @@ -516,7 +516,7 @@ describe('auth store', () => { it('initializes if not started', async () => { mockGetProfileApi.mockResolvedValue({ data: PROFILE_RESPONSE, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); const result = await authStore.waitForInit(); expect(result).toBe(true); @@ -528,7 +528,7 @@ describe('auth store', () => { it('calls clearUserSettings on logout', async () => { mockLogoutApi.mockResolvedValue({ data: {}, error: null }); - const { authStore } = await import('$stores/auth.svelte'); + const { authStore } = await import('#stores/auth.svelte.ts'); await authStore.logout(); expect(mockClearUserSettings).toHaveBeenCalled(); diff --git a/frontend/src/stores/__tests__/errorStore.test.ts b/frontend/src/stores/__tests__/errorStore.test.ts deleted file mode 100644 index d7d5caab2..000000000 --- a/frontend/src/stores/__tests__/errorStore.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; - -describe('errorStore', () => { - beforeEach(async () => { - vi.resetModules(); - vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('initial state', () => { - it('has null error initially', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - expect(appError.current).toBe(null); - }); - }); - - describe('setError', () => { - it('sets an Error object', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - const error = new Error('Test error message'); - appError.setError(error); - - expect(appError.current).not.toBe(null); - expect(appError.current?.error).toBe(error); - expect(appError.current?.title).toBeUndefined(); - }); - - it('sets a string error', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - appError.setError('String error message'); - - expect(appError.current).not.toBe(null); - expect(appError.current?.error).toBe('String error message'); - }); - - it('sets error with title', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - appError.setError('Error with title', 'Custom Title'); - - expect(appError.current).not.toBe(null); - expect(appError.current?.error).toBe('Error with title'); - expect(appError.current?.title).toBe('Custom Title'); - }); - - it('logs error via consola', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - const consoleSpy = vi.spyOn(console, 'error'); - appError.setError('Logged error', 'Error Title'); - - const args = consoleSpy.mock.calls[0]!; - expect(args[0]).toContain('ErrorStore'); - expect(args[0]).toContain('Error Title'); - expect(args[args.length - 1]).toBe('Logged error'); - }); - - it('logs error without title via consola', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - const consoleSpy = vi.spyOn(console, 'error'); - appError.setError('Logged error without title'); - - const args = consoleSpy.mock.calls[0]!; - expect(args[0]).toContain('ErrorStore'); - expect(args[0]).toContain('Error:'); - expect(args[args.length - 1]).toBe('Logged error without title'); - }); - - it('overwrites previous error', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - appError.setError('First error'); - appError.setError('Second error'); - - expect(appError.current?.error).toBe('Second error'); - }); - }); - - describe('clear', () => { - it('clears the error', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - appError.setError('Error to clear'); - expect(appError.current).not.toBe(null); - - appError.clear(); - expect(appError.current).toBe(null); - }); - - it('does nothing when already null', async () => { - const { appError } = await import('$stores/errorStore.svelte'); - appError.clear(); - expect(appError.current).toBe(null); - }); - }); -}); diff --git a/frontend/src/stores/__tests__/notificationStore.test.ts b/frontend/src/stores/__tests__/notificationStore.test.ts index c401e423e..439bac551 100644 --- a/frontend/src/stores/__tests__/notificationStore.test.ts +++ b/frontend/src/stores/__tests__/notificationStore.test.ts @@ -1,20 +1,20 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { createMockNotification } from '$test/test-utils'; +import { createMockNotification } from '#test/test-utils.ts'; const mockGetNotifications = vi.fn(); const mockMarkRead = vi.fn(); const mockMarkAllRead = vi.fn(); const mockDeleteNotification = vi.fn(); -vi.mock('$lib/api', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('#lib/api/index.ts', async (importOriginal) => ({ + ...(await importOriginal()), getNotificationsApiV1NotificationsGet: (...args: unknown[]) => mockGetNotifications(...args), markNotificationReadApiV1NotificationsNotificationIdReadPut: (...args: unknown[]) => mockMarkRead(...args), markAllReadApiV1NotificationsMarkAllReadPost: (...args: unknown[]) => mockMarkAllRead(...args), deleteNotificationApiV1NotificationsNotificationIdDelete: (...args: unknown[]) => mockDeleteNotification(...args), })); -vi.mock('$lib/api-interceptors', () => ({ +vi.mock('#lib/api-interceptors.ts', () => ({ getErrorMessage: (err: unknown, fallback = 'An error occurred') => { if (!err) return fallback; if (typeof err === 'string') return err; @@ -51,17 +51,17 @@ describe('notificationStore', () => { describe('initial state', () => { it('has empty notifications array', async () => { - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); expect(notificationStore.notifications).toEqual([]); }); it('has loading false', async () => { - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); expect(notificationStore.loading).toBe(false); }); it('has null error', async () => { - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); expect(notificationStore.error).toBe(null); }); }); @@ -75,7 +75,7 @@ describe('notificationStore', () => { }); }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); const loadPromise = notificationStore.load(); capturedLoading = notificationStore.loading; @@ -94,7 +94,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.notifications).toHaveLength(2); @@ -107,7 +107,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.loading).toBe(false); @@ -120,7 +120,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); const result = await notificationStore.load(); expect(result).toEqual(notifications); @@ -132,7 +132,7 @@ describe('notificationStore', () => { error: { detail: [{ msg: 'Failed to fetch' }] }, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.error).toBe('Failed to fetch'); @@ -144,7 +144,7 @@ describe('notificationStore', () => { error: { detail: [{ msg: 'Error' }] }, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); const result = await notificationStore.load(); expect(result).toEqual([]); @@ -156,7 +156,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(50); expect(mockGetNotifications).toHaveBeenCalledWith({ @@ -170,7 +170,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(20, { include_tags: ['important'], exclude_tags: ['spam'], @@ -195,7 +195,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const newNotification = createMockNotification({ notification_id: 'new', subject: 'New' }); @@ -216,7 +216,7 @@ describe('notificationStore', () => { }); mockMarkRead.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const result = await notificationStore.markAsRead('n1'); @@ -232,7 +232,7 @@ describe('notificationStore', () => { }); mockMarkRead.mockResolvedValue({ error: { detail: 'Failed' } }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const result = await notificationStore.markAsRead('n1'); @@ -244,7 +244,7 @@ describe('notificationStore', () => { it('calls API with correct notification ID', async () => { mockMarkRead.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.markAsRead('test-id-123'); expect(mockMarkRead).toHaveBeenCalledWith({ @@ -266,7 +266,7 @@ describe('notificationStore', () => { }); mockMarkAllRead.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const result = await notificationStore.markAllAsRead(); @@ -278,7 +278,7 @@ describe('notificationStore', () => { it('returns false on failure', async () => { mockMarkAllRead.mockResolvedValue({ error: { detail: 'Failed' } }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); const result = await notificationStore.markAllAsRead(); expect(result).toBe(false); @@ -298,7 +298,7 @@ describe('notificationStore', () => { }); mockDeleteNotification.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const result = await notificationStore.delete('n1'); @@ -315,7 +315,7 @@ describe('notificationStore', () => { }); mockDeleteNotification.mockResolvedValue({ error: { detail: 'Failed' } }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); const result = await notificationStore.delete('n1'); @@ -327,7 +327,7 @@ describe('notificationStore', () => { it('calls API with correct notification ID', async () => { mockDeleteNotification.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.delete('delete-me-123'); expect(mockDeleteNotification).toHaveBeenCalledWith({ @@ -345,7 +345,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.notifications).toHaveLength(2); @@ -362,7 +362,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.refresh(); expect(mockGetNotifications).toHaveBeenCalledWith({ @@ -385,7 +385,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.unreadCount).toBe(2); @@ -400,7 +400,7 @@ describe('notificationStore', () => { error: null, }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.unreadCount).toBe(0); @@ -416,7 +416,7 @@ describe('notificationStore', () => { }); mockMarkRead.mockResolvedValue({ error: null }); - const { notificationStore } = await import('$stores/notificationStore.svelte'); + const { notificationStore } = await import('#stores/notificationStore.svelte.ts'); await notificationStore.load(); expect(notificationStore.unreadCount).toBe(1); diff --git a/frontend/src/stores/__tests__/theme.test.ts b/frontend/src/stores/__tests__/theme.test.ts index 707938153..658723b38 100644 --- a/frontend/src/stores/__tests__/theme.test.ts +++ b/frontend/src/stores/__tests__/theme.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -vi.mock('$lib/user-settings', () => ({ +vi.mock('#lib/user-settings.ts', () => ({ saveUserSettings: vi.fn().mockResolvedValue(true), })); -vi.mock('$stores/auth.svelte', () => ({ +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: { isAuthenticated: false, }, @@ -31,51 +31,51 @@ describe('theme store', () => { describe('initial state', () => { it('defaults to auto when no stored value', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); expect(themeStore.value).toBe('auto'); }); it('loads stored theme from localStorage', async () => { localStorageData['app-theme'] = 'dark'; - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); expect(themeStore.value).toBe('dark'); }); it('ignores invalid stored values', async () => { localStorageData['app-theme'] = 'invalid'; - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); expect(themeStore.value).toBe('auto'); }); it('accepts light theme from storage', async () => { localStorageData['app-theme'] = 'light'; - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); expect(themeStore.value).toBe('light'); }); }); describe('setTheme', () => { it('updates the store value', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('dark'); expect(themeStore.value).toBe('dark'); }); it('persists to localStorage', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('dark'); expect(localStorage.setItem).toHaveBeenCalledWith('app-theme', 'dark'); }); it('applies dark class when set to dark', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('dark'); expect(document.documentElement.classList.contains('dark')).toBe(true); }); it('removes dark class when set to light', async () => { document.documentElement.classList.add('dark'); - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('light'); expect(document.documentElement.classList.contains('dark')).toBe(false); }); @@ -84,7 +84,7 @@ describe('theme store', () => { describe('toggleTheme', () => { it('cycles from light to dark', async () => { localStorageData['app-theme'] = 'light'; - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.toggleTheme(); expect(themeStore.value).toBe('dark'); @@ -92,20 +92,20 @@ describe('theme store', () => { it('cycles from dark to auto', async () => { localStorageData['app-theme'] = 'dark'; - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.toggleTheme(); expect(themeStore.value).toBe('auto'); }); it('cycles from auto to light', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.toggleTheme(); expect(themeStore.value).toBe('light'); }); it('completes full cycle', async () => { - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); expect(themeStore.value).toBe('auto'); themeStore.toggleTheme(); @@ -119,7 +119,7 @@ describe('theme store', () => { describe('setTheme export', () => { it('sets theme to specified value', async () => { - const { themeStore, setTheme } = await import('$stores/theme.svelte'); + const { themeStore, setTheme } = await import('#stores/theme.svelte.ts'); setTheme('dark'); expect(themeStore.value).toBe('dark'); @@ -132,7 +132,7 @@ describe('theme store', () => { }); it('persists to localStorage', async () => { - const { setTheme } = await import('$stores/theme.svelte'); + const { setTheme } = await import('#stores/theme.svelte.ts'); setTheme('dark'); expect(localStorage.setItem).toHaveBeenCalledWith('app-theme', 'dark'); }); @@ -151,7 +151,7 @@ describe('theme store', () => { dispatchEvent: vi.fn(), })); - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('auto'); expect(document.documentElement.classList.contains('dark')).toBe(false); @@ -170,7 +170,7 @@ describe('theme store', () => { })); vi.resetModules(); - const { themeStore } = await import('$stores/theme.svelte'); + const { themeStore } = await import('#stores/theme.svelte.ts'); themeStore.setTheme('auto'); expect(document.documentElement.classList.contains('dark')).toBe(true); diff --git a/frontend/src/stores/__tests__/userSettings.test.ts b/frontend/src/stores/__tests__/userSettings.test.ts index 20febbb8b..71406fe5b 100644 --- a/frontend/src/stores/__tests__/userSettings.test.ts +++ b/frontend/src/stores/__tests__/userSettings.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import type { UserSettings } from '$lib/api'; +import type { UserSettings } from '#lib/api/index.ts'; const DEFAULTS = { font_size: 14, @@ -15,7 +15,7 @@ describe('userSettings store', () => { }); it('starts as null', async () => { - const { userSettingsStore } = await import('$stores/userSettings.svelte'); + const { userSettingsStore } = await import('#stores/userSettings.svelte.ts'); expect(userSettingsStore.settings).toBeNull(); }); @@ -23,13 +23,14 @@ describe('userSettings store', () => { ['object', { editor: { font_size: 16 } } as UserSettings], ['null', null], ])('setUserSettings accepts %s', async (_, value) => { - const { userSettingsStore, setUserSettings } = await import('$stores/userSettings.svelte'); + const { userSettingsStore, setUserSettings } = await import('#stores/userSettings.svelte.ts'); setUserSettings(value); expect(userSettingsStore.settings).toEqual(value); }); it('clearUserSettings resets to null', async () => { - const { userSettingsStore, setUserSettings, clearUserSettings } = await import('$stores/userSettings.svelte'); + const { userSettingsStore, setUserSettings, clearUserSettings } = + await import('#stores/userSettings.svelte.ts'); setUserSettings({ editor: { font_size: 20 } } as UserSettings); clearUserSettings(); expect(userSettingsStore.settings).toBeNull(); @@ -37,7 +38,7 @@ describe('userSettings store', () => { describe('editorSettings (derived)', () => { it('returns defaults when userSettings is null', async () => { - const { userSettingsStore } = await import('$stores/userSettings.svelte'); + const { userSettingsStore } = await import('#stores/userSettings.svelte.ts'); expect(userSettingsStore.editorSettings).toEqual(DEFAULTS); }); @@ -53,14 +54,14 @@ describe('userSettings store', () => { { font_size: 18, tab_size: 8, use_tabs: true, word_wrap: false, show_line_numbers: false }, ], ])('returns %s from API', async (_, editor, expected) => { - const { userSettingsStore, setUserSettings } = await import('$stores/userSettings.svelte'); + const { userSettingsStore, setUserSettings } = await import('#stores/userSettings.svelte.ts'); setUserSettings({ editor } as UserSettings); expect(userSettingsStore.editorSettings).toEqual(expected); }); it('reverts to defaults when cleared', async () => { const { userSettingsStore, setUserSettings, clearUserSettings } = - await import('$stores/userSettings.svelte'); + await import('#stores/userSettings.svelte.ts'); setUserSettings({ editor: { font_size: 20 } } as UserSettings); clearUserSettings(); expect(userSettingsStore.editorSettings).toEqual(DEFAULTS); diff --git a/frontend/src/stores/auth.svelte.ts b/frontend/src/stores/auth.svelte.ts index ffa314925..f6d354baa 100644 --- a/frontend/src/stores/auth.svelte.ts +++ b/frontend/src/stores/auth.svelte.ts @@ -3,9 +3,9 @@ import { logoutApiV1AuthLogoutPost, getCurrentUserProfileApiV1AuthMeGet, type UserRole, -} from '$lib/api'; -import { clearUserSettings } from '$stores/userSettings.svelte'; -import { logger } from '$lib/logger'; +} from '#lib/api/index.ts'; +import { clearUserSettings } from '#stores/userSettings.svelte.ts'; +import { logger } from '#lib/logger.ts'; const log = logger.withTag('Auth'); @@ -134,7 +134,7 @@ class AuthStore { try { // Dynamic import to avoid circular dependency: // auth.svelte.ts -> user-settings.ts -> auth.svelte.ts - const { loadUserSettings } = await import('$lib/user-settings'); + const { loadUserSettings } = await import('#lib/user-settings.ts'); await loadUserSettings(); } catch (err) { log.warn('Failed to load user settings:', err); diff --git a/frontend/src/stores/errorStore.svelte.ts b/frontend/src/stores/errorStore.svelte.ts deleted file mode 100644 index 1818b54a7..000000000 --- a/frontend/src/stores/errorStore.svelte.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { logger } from '$lib/logger'; - -const log = logger.withTag('ErrorStore'); - -export interface AppError { - error: Error | string; - title?: string; -} - -class ErrorStore { - current = $state(null); - - setError(error: Error | string, title?: string): void { - log.error(title || 'Error:', error); - this.current = { error, title }; - } - - clear(): void { - this.current = null; - } -} - -export const appError = new ErrorStore(); diff --git a/frontend/src/stores/notificationStore.svelte.ts b/frontend/src/stores/notificationStore.svelte.ts index de80f5d8a..35d6eb21f 100644 --- a/frontend/src/stores/notificationStore.svelte.ts +++ b/frontend/src/stores/notificationStore.svelte.ts @@ -5,8 +5,8 @@ import { deleteNotificationApiV1NotificationsNotificationIdDelete, type GetNotificationsApiV1NotificationsGetData, type NotificationResponse, -} from '$lib/api'; -import { getErrorMessage } from '$lib/api-interceptors'; +} from '#lib/api/index.ts'; +import { getErrorMessage } from '#lib/api-interceptors.ts'; type NotificationQueryOpts = Omit, 'limit' | 'offset'>; diff --git a/frontend/src/stores/theme.svelte.ts b/frontend/src/stores/theme.svelte.ts index d1008c8f6..5cbda23c7 100644 --- a/frontend/src/stores/theme.svelte.ts +++ b/frontend/src/stores/theme.svelte.ts @@ -1,5 +1,5 @@ -import { Theme } from '$lib/api'; -import { logger } from '$lib/logger'; +import { Theme } from '#lib/api/index.ts'; +import { logger } from '#lib/logger.ts'; const log = logger.withTag('Theme'); @@ -31,10 +31,12 @@ let saveUserSettingsFn: ((partial: { theme?: Theme }) => Promise) | nul let authStoreRef: { isAuthenticated: boolean | null } | null = null; if (browser) { - void Promise.all([import('$lib/user-settings'), import('$stores/auth.svelte')]).then(([userSettings, auth]) => { - saveUserSettingsFn = userSettings.saveUserSettings; - authStoreRef = auth.authStore; - }); + void Promise.all([import('#lib/user-settings.ts'), import('#stores/auth.svelte.ts')]).then( + ([userSettings, auth]) => { + saveUserSettingsFn = userSettings.saveUserSettings; + authStoreRef = auth.authStore; + }, + ); } class ThemeStore { diff --git a/frontend/src/stores/userSettings.svelte.ts b/frontend/src/stores/userSettings.svelte.ts index 5c449e8e7..2de6b6a7d 100644 --- a/frontend/src/stores/userSettings.svelte.ts +++ b/frontend/src/stores/userSettings.svelte.ts @@ -1,4 +1,4 @@ -import type { UserSettings, EditorSettingsOutput } from '$lib/api'; +import type { UserSettings, EditorSettingsOutput } from '#lib/api/index.ts'; const FALLBACK_EDITOR_SETTINGS: EditorSettingsOutput = { font_size: 14, diff --git a/frontend/src/utils/__tests__/meta.test.ts b/frontend/src/utils/__tests__/meta.test.ts deleted file mode 100644 index 7745b88a0..000000000 --- a/frontend/src/utils/__tests__/meta.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { updateMetaTags, pageMeta } from '$utils/meta'; - -describe('meta utilities', () => { - let originalTitle: string; - let metaDescription: HTMLMetaElement | null; - - beforeEach(() => { - originalTitle = document.title; - metaDescription = document.querySelector('meta[name="description"]'); - document.title = 'Original Title'; - - const existingMeta = document.querySelector('meta[name="description"]'); - if (existingMeta) { - existingMeta.remove(); - } - }); - - afterEach(() => { - document.title = originalTitle; - - const testMeta = document.querySelector('meta[name="description"]'); - if (testMeta) { - testMeta.remove(); - } - - if (metaDescription) { - document.head.appendChild(metaDescription); - } - }); - - describe('updateMetaTags', () => { - it('updates document title with suffix', () => { - updateMetaTags('Test Page'); - - expect(document.title).toBe('Test Page | Integr8sCode'); - }); - - it('does not update title when not provided', () => { - document.title = 'Keep This Title'; - - updateMetaTags(undefined, 'Some description'); - - expect(document.title).toBe('Keep This Title'); - }); - - it('creates meta description if it does not exist', () => { - expect(document.querySelector('meta[name="description"]')).toBe(null); - - updateMetaTags(undefined, 'New description'); - - const meta = document.querySelector('meta[name="description"]'); - expect(meta).not.toBe(null); - expect(meta?.getAttribute('content')).toBe('New description'); - }); - - it('updates existing meta description', () => { - const existingMeta = document.createElement('meta'); - existingMeta.setAttribute('name', 'description'); - existingMeta.setAttribute('content', 'Old description'); - document.head.appendChild(existingMeta); - - updateMetaTags(undefined, 'Updated description'); - - const meta = document.querySelector('meta[name="description"]'); - expect(meta?.getAttribute('content')).toBe('Updated description'); - }); - - it('does not update description when not provided', () => { - const existingMeta = document.createElement('meta'); - existingMeta.setAttribute('name', 'description'); - existingMeta.setAttribute('content', 'Keep this'); - document.head.appendChild(existingMeta); - - updateMetaTags('New Title'); - - const meta = document.querySelector('meta[name="description"]'); - expect(meta?.getAttribute('content')).toBe('Keep this'); - }); - - it('updates both title and description', () => { - updateMetaTags('Full Update', 'Complete description'); - - expect(document.title).toBe('Full Update | Integr8sCode'); - - const meta = document.querySelector('meta[name="description"]'); - expect(meta?.getAttribute('content')).toBe('Complete description'); - }); - - it('handles empty strings', () => { - document.title = 'Original'; - - updateMetaTags('', ''); - - // Empty string is falsy, so title should not change - expect(document.title).toBe('Original'); - }); - }); - - describe('pageMeta', () => { - it('contains home page meta', () => { - expect(pageMeta.home).toBeDefined(); - expect(pageMeta.home.title).toBe('Home'); - expect(pageMeta.home.description).toContain('Integr8sCode'); - }); - - it('contains editor page meta', () => { - expect(pageMeta.editor).toBeDefined(); - expect(pageMeta.editor.title).toBe('Code Editor'); - expect(pageMeta.editor.description).toContain('code editor'); - }); - - it('contains login page meta', () => { - expect(pageMeta.login).toBeDefined(); - expect(pageMeta.login.title).toBe('Login'); - expect(pageMeta.login.description).toContain('Sign in'); - }); - - it('contains register page meta', () => { - expect(pageMeta.register).toBeDefined(); - expect(pageMeta.register.title).toBe('Register'); - expect(pageMeta.register.description).toContain('account'); - }); - - it('all pages have title and description', () => { - Object.entries(pageMeta).forEach(([key, meta]) => { - expect(meta.title, `${key} should have title`).toBeDefined(); - expect(meta.title.length, `${key} title should not be empty`).toBeGreaterThan(0); - expect(meta.description, `${key} should have description`).toBeDefined(); - expect(meta.description.length, `${key} description should not be empty`).toBeGreaterThan(0); - }); - }); - - it('can be used with updateMetaTags', () => { - const { title, description } = pageMeta.home; - - updateMetaTags(title, description); - - expect(document.title).toBe('Home | Integr8sCode'); - - const meta = document.querySelector('meta[name="description"]'); - expect(meta?.getAttribute('content')).toBe(description); - }); - }); -}); diff --git a/frontend/src/utils/meta.ts b/frontend/src/utils/meta.ts deleted file mode 100644 index b9c1f029c..000000000 --- a/frontend/src/utils/meta.ts +++ /dev/null @@ -1,46 +0,0 @@ -export function updateMetaTags(title?: string, description?: string): void { - if (title) { - document.title = `${title} | Integr8sCode`; - } - - if (description) { - let metaDescription = document.querySelector('meta[name="description"]'); - if (!metaDescription) { - metaDescription = document.createElement('meta'); - metaDescription.setAttribute('name', 'description'); - document.head.appendChild(metaDescription); - } - metaDescription.setAttribute('content', description); - } -} - -export const pageMeta = { - home: { - title: 'Home', - description: - 'Integr8sCode - Write, compile, and manage code directly in your browser with our powerful online development environment', - }, - editor: { - title: 'Code Editor', - description: - 'Online code editor with syntax highlighting, multi-language support, and real-time compilation. Write and test your code instantly in the browser', - }, - login: { - title: 'Login', - description: - 'Sign in to Integr8sCode to access your saved projects, code snippets, and personalized development environment', - }, - register: { - title: 'Register', - description: - 'Create a free Integr8sCode account to save your projects, collaborate with others, and access advanced coding features', - }, - privacy: { - title: 'Privacy Policy', - description: 'Privacy policy for Integr8sCode - how we handle your data, GDPR compliance, and your rights', - }, - settings: { - title: 'Settings', - description: 'Manage your Integr8sCode account settings, editor preferences, and notification configuration', - }, -}; diff --git a/frontend/public/favicon.png b/frontend/static/favicon.png similarity index 100% rename from frontend/public/favicon.png rename to frontend/static/favicon.png diff --git a/frontend/public/robots.txt b/frontend/static/robots.txt similarity index 100% rename from frontend/public/robots.txt rename to frontend/static/robots.txt diff --git a/frontend/public/sitemap.xml b/frontend/static/sitemap.xml similarity index 100% rename from frontend/public/sitemap.xml rename to frontend/static/sitemap.xml diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index f7bdd44cc..000000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - compilerOptions: { - dev: true, - runes: true, - }, -}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 4b8fff208..06024529b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,41 +1,13 @@ { + "extends": "$app/tsconfig", "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": false, - "sourceMap": true, - "resolveJsonModule": true, - "allowJs": true, "checkJs": false, - "isolatedModules": true, - "verbatimModuleSyntax": true, - "rootDir": ".", - "outDir": "./public/build", - "paths": { - "$lib": ["./src/lib"], - "$lib/*": ["./src/lib/*"], - "$components": ["./src/components"], - "$components/*": ["./src/components/*"], - "$stores": ["./src/stores"], - "$stores/*": ["./src/stores/*"], - "$routes": ["./src/routes"], - "$routes/*": ["./src/routes/*"], - "$utils": ["./src/utils"], - "$utils/*": ["./src/utils/*"], - "$styles": ["./src/styles"], - "$styles/*": ["./src/styles/*"], - "$test": ["./src/__tests__"], - "$test/*": ["./src/__tests__/*"] - } + "sourceMap": true }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"], - "exclude": ["node_modules", "public"] + "include": ["src"], + "exclude": ["node_modules", "build"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 000000000..d4f1d6e26 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,50 @@ +import fs from 'node:fs'; +import adapter from '@sveltejs/adapter-static'; +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig, type ServerOptions } from 'vite'; + +// The dev server is useful inside the compose network, where the TLS certs and the backend exist +function devServer(): ServerOptions { + const keyFile = process.env.SSL_KEY_FILE ?? './certs/server.key'; + const certFile = process.env.SSL_CERT_FILE ?? './certs/server.crt'; + const tls = fs.existsSync(keyFile) && fs.existsSync(certFile); + return { + host: '0.0.0.0', + port: 5001, + strictPort: true, + https: tls ? { key: fs.readFileSync(keyFile), cert: fs.readFileSync(certFile) } : undefined, + proxy: { + '/api': { target: 'https://backend:443', secure: false }, + }, + }; +} + +export default defineConfig(({ command }) => ({ + plugins: [ + tailwindcss(), + sveltekit({ + adapter: adapter({ fallback: '200.html' }), + // --8<-- [start:csp] + // Prerendered shells can't carry a per-request nonce, so Kit hashes its inline scripts into a meta CSP; + // nginx adds frame-ancestors, which a meta CSP can't express + csp: { + mode: 'hash', + directives: { + 'default-src': ['self'], + 'script-src': ['self'], + 'style-src-elem': ['self', 'https://fonts.googleapis.com'], + 'style-src-attr': ['unsafe-inline'], + 'img-src': ['self', 'data:', 'blob:'], + 'font-src': ['self', 'data:', 'https://fonts.gstatic.com'], + 'connect-src': ['self'], + 'object-src': ['none'], + 'base-uri': ['self'], + 'form-action': ['self'], + }, + }, + // --8<-- [end:csp] + }), + ], + server: command === 'serve' && !process.env.VITEST ? devServer() : undefined, +})); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index b743f346a..63511bdb3 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -1,11 +1,11 @@ import { defineConfig } from 'vitest/config'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { sveltekit } from '@sveltejs/kit/vite'; import { svelteTesting } from '@testing-library/svelte/vite'; // --8<-- [start:config] export default defineConfig({ plugins: [ - svelte({ compilerOptions: { hmr: !process.env.VITEST } }), + sveltekit(), svelteTesting(), ], test: { @@ -44,15 +44,6 @@ export default defineConfig({ }, resolve: { conditions: ['browser'], - alias: { - $lib: '/src/lib', - $components: '/src/components', - $stores: '/src/stores', - $routes: '/src/routes', - $utils: '/src/utils', - $styles: '/src/styles', - $test: '/src/__tests__', - }, }, }); // --8<-- [end:config] diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts index 721494d4d..6fb87df4c 100644 --- a/frontend/vitest.setup.ts +++ b/frontend/vitest.setup.ts @@ -5,8 +5,8 @@ import { cleanup } from '@testing-library/svelte'; vi.useFakeTimers({ shouldAdvanceTime: true }); // SDK calls become mocks; enums, Zod schemas and the client stay real -vi.mock('$lib/api', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('#lib/api/index.ts', async (importOriginal) => { + const actual = await importOriginal(); return Object.fromEntries( Object.entries(actual).map(([name, value]) => [name, typeof value === 'function' ? vi.fn() : value]), ); @@ -17,9 +17,9 @@ vi.mock('svelte-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }, })); -vi.mock('@mateothegreat/svelte5-router', () => ({ route: () => {}, goto: vi.fn() })); +vi.mock('$app/navigation', () => ({ goto: vi.fn() })); -vi.mock('$lib/api-interceptors', () => ({ +vi.mock('#lib/api-interceptors.ts', () => ({ unwrap: vi.fn((result: { data?: unknown; error?: unknown }) => { if (result.error) throw result.error; if (result.data === undefined) throw new Error('Unexpected empty response'); From cd463cf4f6a54431608ca3c949e7c9d6dc3d3c94 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 11 Oct 2026 18:01:32 +0200 Subject: [PATCH 2/2] fix: sync SvelteKit before generating the API client; cover the new route files openapi-ts reads tsconfig.json, which extends the $app/tsconfig that only svelte-kit sync writes, so the Generated API job failed on a clean install. Also use replaceAll and .at() now that the tsconfig targets ESNext, and test the root layout, the guard layouts and the /admin redirect. --- frontend/package.json | 2 +- .../src/lib/admin/rate-limits/rateLimits.ts | 2 +- frontend/src/lib/api-interceptors.ts | 2 +- frontend/src/lib/formatters.ts | 2 +- frontend/src/lib/validation.ts | 2 +- frontend/src/routes/__tests__/layout.test.ts | 46 +++++++++++++++++++ .../src/routes/__tests__/route-files.test.ts | 41 +++++++++++++++++ 7 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 frontend/src/routes/__tests__/layout.test.ts create mode 100644 frontend/src/routes/__tests__/route-files.test.ts diff --git a/frontend/package.json b/frontend/package.json index 8183bb598..a062f6279 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,7 @@ "build": "vite build", "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", - "generate:api": "openapi-ts", + "generate:api": "svelte-kit sync && openapi-ts", "lint": "svelte-kit sync && eslint src --ext .ts,.svelte", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", "test": "vitest run", diff --git a/frontend/src/lib/admin/rate-limits/rateLimits.ts b/frontend/src/lib/admin/rate-limits/rateLimits.ts index 6f42518a1..6480e8c8d 100644 --- a/frontend/src/lib/admin/rate-limits/rateLimits.ts +++ b/frontend/src/lib/admin/rate-limits/rateLimits.ts @@ -30,7 +30,7 @@ const ENDPOINT_GROUP_PATTERNS: Array<{ pattern: RegExp; group: EndpointGroup }> export function detectGroupFromEndpoint(endpoint: string): EndpointGroup { // Strip regex anchors: leading ^, trailing $, and .* wildcards - const cleanEndpoint = endpoint.replace(/^\^/, '').replace(/\$$/, '').replace(/\.\*/g, ''); + const cleanEndpoint = endpoint.replace(/^\^/, '').replace(/\$$/, '').replaceAll('.*', ''); for (const { pattern, group } of ENDPOINT_GROUP_PATTERNS) { if (pattern.test(cleanEndpoint)) return group; } diff --git a/frontend/src/lib/api-interceptors.ts b/frontend/src/lib/api-interceptors.ts index f6fbcd75f..8735977ed 100644 --- a/frontend/src/lib/api-interceptors.ts +++ b/frontend/src/lib/api-interceptors.ts @@ -29,7 +29,7 @@ function formatValidationErrors(errors: ValidationError[]): string { return errors .map((e) => { if (!e.loc.length) return e.msg; - const field = e.loc[e.loc.length - 1]; + const field = e.loc.at(-1); return `${typeof field === 'string' ? field : 'field'}: ${e.msg}`; }) .join(', '); diff --git a/frontend/src/lib/formatters.ts b/frontend/src/lib/formatters.ts index bde425093..b9f844696 100644 --- a/frontend/src/lib/formatters.ts +++ b/frontend/src/lib/formatters.ts @@ -142,6 +142,6 @@ export function truncate(str: string | null | undefined, maxLength = 50): string * @returns Human-readable label */ export function formatEnumLabel(value: string): string { - const words = value.replace(/_/g, ' ').toLowerCase(); + const words = value.replaceAll('_', ' ').toLowerCase(); return words.charAt(0).toUpperCase() + words.slice(1); } diff --git a/frontend/src/lib/validation.ts b/frontend/src/lib/validation.ts index e91710f82..f1e2479d6 100644 --- a/frontend/src/lib/validation.ts +++ b/frontend/src/lib/validation.ts @@ -10,7 +10,7 @@ import { formatEnumLabel } from '#lib/formatters.ts'; export function firstIssue(error: core.$ZodError): string { const issue = error.issues[0]; if (!issue) return 'Invalid input'; - const last = issue.path[issue.path.length - 1]; + const last = issue.path.at(-1); const field = typeof last === 'string' ? formatEnumLabel(last) : 'Value'; switch (issue.code) { case 'too_small': diff --git a/frontend/src/routes/__tests__/layout.test.ts b/frontend/src/routes/__tests__/layout.test.ts new file mode 100644 index 000000000..1682e5836 --- /dev/null +++ b/frontend/src/routes/__tests__/layout.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/svelte'; +import { createRawSnippet } from 'svelte'; +import Layout from '#routes/+layout.svelte'; + +const mocks = vi.hoisted(() => ({ initialize: vi.fn() })); + +vi.mock('#stores/auth.svelte.ts', () => ({ authStore: { initialize: mocks.initialize } })); +vi.mock('#components/Header.svelte', async () => { + const utils = await import('#test/test-utils.ts'); + return { default: utils.createMockNamedComponents({ default: '

header
' }).default }; +}); +vi.mock('#components/Footer.svelte', async () => { + const utils = await import('#test/test-utils.ts'); + return { default: utils.createMockNamedComponents({ default: '
footer
' }).default }; +}); +vi.mock('svelte-sonner', async () => { + const utils = await import('#test/test-utils.ts'); + return { + toast: { success: vi.fn(), error: vi.fn() }, + Toaster: utils.createMockNamedComponents({ default: '' }).default, + }; +}); + +const children = createRawSnippet(() => ({ render: () => '

page body

' })); + +describe('root layout', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows the page once the auth check finishes', async () => { + mocks.initialize.mockResolvedValue(true); + render(Layout, { props: { children } }); + + await waitFor(() => expect(screen.getByText('page body')).toBeInTheDocument()); + expect(mocks.initialize).toHaveBeenCalledOnce(); + }); + + it('still shows the page when the auth check fails', async () => { + mocks.initialize.mockRejectedValue(new Error('down')); + render(Layout, { props: { children } }); + + await waitFor(() => expect(screen.getByText('page body')).toBeInTheDocument()); + }); +}); diff --git a/frontend/src/routes/__tests__/route-files.test.ts b/frontend/src/routes/__tests__/route-files.test.ts new file mode 100644 index 000000000..35165d2e5 --- /dev/null +++ b/frontend/src/routes/__tests__/route-files.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { isRedirect } from '@sveltejs/kit'; +import { requireAdmin, requireAuth } from '#lib/auth-guard.ts'; +import { ssr } from '#routes/+layout.ts'; +import { load as authGroupLoad } from '#routes/(auth)/+layout.ts'; +import { load as adminLoad } from '#routes/admin/+layout.ts'; +import { load as adminIndexLoad } from '#routes/admin/+page.ts'; + +vi.mock('#lib/auth-guard.ts', () => ({ requireAuth: vi.fn(), requireAdmin: vi.fn() })); + +const url = new URL('https://localhost/editor'); + +describe('route files', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders every page in the browser only', () => { + expect(ssr).toBe(false); + }); + + it('guards the (auth) group with requireAuth', async () => { + await authGroupLoad({ url } as Parameters[0]); + expect(vi.mocked(requireAuth)).toHaveBeenCalledWith(url); + }); + + it('guards the admin pages with requireAdmin', async () => { + await adminLoad({ url } as Parameters[0]); + expect(vi.mocked(requireAdmin)).toHaveBeenCalledWith(url); + }); + + it('sends /admin to the event browser', () => { + let thrown: unknown; + try { + adminIndexLoad(); + } catch (e) { + thrown = e; + } + expect(isRedirect(thrown) && thrown.location).toBe('/admin/events'); + }); +});