This is the Stackable Unified Data Platform UI - a SvelteKit application that serves as the first module of a unified platform UI for Stackable's data infrastructure. The initial module is a Trino SQL query editor with SSO, authorisation, and catalogue browsing.
The application is designed with a plugin/module architecture so future Stackable services (trino-lb queue management, etc.) can be integrated as additional modules.
- SvelteKit with Svelte 5 - Always use Svelte 5 syntax with runes (
$props,$state,$derived, etc.) - Tailwind CSS v4 - No tailwind.config.js file (uses CSS-based configuration)
- DaisyUI - Use DaisyUI components/classes where possible for consistent UI - This is DaisyUI v5! A lot of classes you know about DON'T EXIST anymore. Check https://daisyui.com/docs/upgrade/?lang=enj if needed
- Icon Library - Use unplugin-icons with the
material-symbolsicon set (@iconify-json/material-symbols). Import icons as virtual Svelte components:import IconMenu from 'virtual:icons/material-symbols/menu'. - Monaco Editor - SQL editor component (dynamic import, SSR-safe)
- ANTLR4 (antlr4ng) - Trino SQL parsing for syntax highlighting and code completion
- zod & superforms - All forms are to use zod & superforms
- Generic OIDC (openid-client) - Authentication via any OIDC provider
- OPA - Authorisation / feature visibility
- prom-client - Prometheus metrics, exposed at
/metrics
This is a single SvelteKit application (not a monorepo).
├── src/
│ ├── lib/ # Shared utilities, components, stores
│ ├── routes/ # SvelteKit routes
│ │ └── (app)/ # Authenticated app routes
│ ├── app.css # Global styles (Tailwind + DaisyUI)
│ ├── app.d.ts # Type declarations
│ ├── app.html # HTML template
│ └── hooks.server.ts # Server hooks (auth middleware)
├── e2e/ # Playwright E2E tests
├── static/ # Static assets
├── docker/Dockerfile # Production container image
└── CLAUDE.md # AI assistant instructions
When introducing shortcuts, known issues, or deferred security work, file an issue with the tech-debt label. Keep it concise: what the issue is, why it is acceptable now, and what the correct long-term fix is.
- The application must work in both Firefox and Chromium-based browsers (Chrome, Edge).
- Do not use browser-specific features or CSS without verifying cross-browser support.
- E2E tests run on both Firefox and Chromium (desktop) plus a mobile Chromium viewport.
- All UI must work in both light and dark mode automatically. Never use hard-coded colours (e.g.,
text-gray-700,bg-white,border-slate-200). Always use DaisyUI semantic colour classes (text-base-content,bg-base-100,bg-base-200,border-base-300,text-primary,bg-error, etc.) and Tailwind opacity modifiers on those classes (text-base-content/60,bg-primary/10) so that colours adapt to the active theme. - All pages and components must be mobile-friendly. Use responsive Tailwind breakpoints (
sm:,md:,lg:) and test layouts at mobile viewport widths. Navigation must be accessible on small screens. - Use Tailwind v4
@sourcedirective for component CSS scanning
The application must comply with BITV 2.0 (German accessibility regulation, based on WCAG 2.1 AA). Key requirements:
- Semantic HTML: Use correct elements (
<nav>,<main>,<header>,<button>,<a>, headings in order). Never use<div>or<span>for interactive elements. - Keyboard navigation: All interactive elements must be reachable and operable via keyboard. Visible focus indicators are required.
- ARIA attributes: Use
aria-label,aria-current,aria-disabled,aria-expanded,rolewhere semantic HTML is insufficient. Do not use ARIA to fix what semantic HTML can handle. - Colour contrast: Text must meet WCAG AA contrast ratios (4.5:1 normal text, 3:1 large text). Never convey information through colour alone.
- Form labels: Every form control must have a visible, associated
<label>(see label-element association rule below). - Alt text: Every
<img>must have a meaningfulaltattribute (oralt=""for decorative images). - Language: The
<html>element must have alangattribute. Uselangattributes on content in other languages. - Screen reader support: Ensure content is announced correctly. Hide decorative elements with
aria-hidden="true".
The project uses pino for structured JSON logging (server-side only).
Two usage patterns:
-
Request-scoped (in
+page.server.ts,+server.ts, hooks) — use the logger fromevent.locals:const log = event.locals.logger; log.info({ catalog_name: name }, 'Loading catalogue');
-
Module-level (singletons, services) — create a child logger:
import { logger } from '$lib/server/logging'; const log = logger.child({ module: 'trino-client' }); log.info({ trino_url: url }, 'Connecting to Trino');
Log level guidance:
trace— request lifecycle noise and highly detailed diagnostics (for example request start/completion for successful requests)debug— verbose operational detail (cache hits, query plans, non-request-flow diagnostics)info— significant business events (user login, query executed, service discovered)warn— recoverable problems needing attention (deprecated config, retry succeeded)error— failures requiring investigation (unhandled exceptions, external service down)
Conventions:
- Context object first, message string second:
log.info({ user_id }, 'User logged in') - Use snake_case field names for queryability:
request_id,user_id,module,duration_ms,status_code,path,method - Never log tokens, credentials, or full request/session objects directly — add redaction paths to
src/lib/server/logging/redaction.ts
- Always use dynamic imports to avoid SSR issues (
import('monaco-editor')) - The editor must be loaded client-side only
- Use the custom
trinosqllanguage registration for Trino SQL support
The grammar lives at src/lib/editor/grammar/SqlBase.g4. The authoritative source is the main Trino repository:
core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4
Fetch the latest version with:
gh api "repos/trinodb/trino/contents/core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4" \
--jq '.download_url' | xargs curl -s -o src/lib/editor/grammar/SqlBase.g4Then regenerate the TypeScript lexer/parser:
npm run generate:antlrAfter regeneration, compare the new token list against src/lib/editor/tokenMap.ts. Any token present in SqlBaseLexer.ts but missing from tokenMap.ts will fall back to 'identifier' scope (no colour). Add missing tokens with an appropriate scope ('keyword', 'delimiter', 'string', etc.).
The app exposes a Prometheus scrape endpoint at /metrics via src/routes/metrics/+server.ts. HTTP request duration is tracked automatically for all routes in hooks.server.ts.
- Adding custom metrics: Define new counters, histograms, or gauges in
src/lib/server/metrics.tsand import them in the relevant server-side code (e.g. track query execution counts, Trino errors, cache hits) - Server-only: Never import from
src/lib/server/metrics.tsin client-side code or.sveltefiles - When to add metrics: New API endpoints, background operations, external service calls, and any operation where latency or error rates are operationally significant
-
Use spaces not tabs
-
Run npm run format
-
Use British English
-
For the server side only: Always include logging at debug and info levels as appropriate
-
Redirects: Never wrap
throw redirect()in try-catch. Put redirect AFTER try-catch to avoid it being caught.try { await operation(); } catch (err) { return fail(500, { error: 'Failed' }); } throw redirect(303, '/path'); // Outside try-catch
-
Public Routes: Update
hooks.server.tswhen adding unauthenticated pages. The/metricsendpoint is permanently public (Prometheus scraping) — never add auth in front of it. -
Label-Element Association: Always explicitly associate
<label>elements with their form controls usingforandidattributes. Generate unique IDs with$props.id()(see https://svelte.dev/docs/svelte/$props#$props.id()). Never rely on implicit association (wrapping the input inside the label).<script> const uid = $props.id(); </script> <label for="{uid}-name" class="label">Name</label> <input id="{uid}-name" class="input" />
-
Modals: Always use a shared
Modalcomponent. Never use raw<dialog>elements directly. Control modals with a boolean$stateandbind:open. -
Date/Time Pickers: Always use a shared
DateTimePickercomponent. Never use native<input type="date">or<input type="datetime-local">elements.
The application uses Paraglide-JS v2 for type-safe, compiler-based internationalisation. English is the default/fallback locale; German is also supported.
- Message files: All user-visible strings go in
messages/en.jsonandmessages/de.json. Both files must always have the same keys. - Import pattern:
import * as m from '$lib/paraglide/messages.js'— then usem.key_name()in templates and script blocks. - Key naming: Use flat keys with a feature prefix and underscores, e.g.
dashboard_welcome,nav_dashboard,header_user_menu. - Reactivity: Data structures containing translated strings (e.g. nav sections, page title maps) must use
$derivedso they update when the locale changes. - No hardcoded strings: Never hardcode user-visible strings in
.sveltefiles. This includes text content,aria-label,title,placeholder, andaltattributes. - Adding new strings: Add the key to both
messages/en.jsonandmessages/de.json, then runnpx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglideto regenerate typed message functions.
The required Node.js version is pinned in .node-version. Use nvm to install and activate it before running any npm commands — npm will hard-fail with engine errors otherwise (.npmrc sets engine-strict=true).
nvm install # installs the version from .node-version
nvm use # activates it in the current shellFor a pre-configured local dev environment using OIDC, deploy & configure Keycloak and Trino on a kind cluster:
./dev/setup.sh # Creates and configures Keycloak. Deploys Trino with OIDC authentication. Writes to .env.development which is used by dev server.npm run dev # Start dev servernpm run build # Build for production
npm run preview # Preview production buildnpm run format # Format code
pre-commit run --all-files # Run all checks: lint (prettier + eslint), type checking,
# markdownlint, yamllint, shellcheck, actionlint, hadolint, helm lintnpm run test:e2e # Run all E2E testsE2E tests use Playwright and are located in e2e/.
- MANDATORY: Always add E2E tests when creating new pages, UI features, or API endpoints. This is not optional - every frontend-visible feature must have corresponding E2E tests before the work is considered complete.
- Always add tests for user flows (navigation, form submissions, filters)
- Always add tests for API endpoints (status codes, response structure)
- Always run
npm run test:e2eafter making UI changes to ensure nothing broke
- Before committing UI changes
- After modifying routes, layouts, or navigation
- After changing form behaviour or validation
- As part of quality checks before pushing
import { test, expect } from '@playwright/test';
test('example test', async ({ page }) => {
await page.goto('/some-page');
await expect(page.locator('h1')).toContainText('Expected Title');
});Tests use a saved session from e2e/.auth/user.json. If tests fail with auth errors, run the auth setup to re-authenticate via OIDC.
- Environment: The app needs a
.envfile for proper env var access - SSR Safety: Monaco Editor and ANTLR must be dynamically imported to avoid SSR issues
- Service Discovery: The app can auto-discover Trino instances via K8s API or use static configuration
For documentation tasks, always update both the primary document AND any related README files that reference it.