Skip to content

Latest commit

 

History

History
314 lines (222 loc) · 17.3 KB

AGENTS.md

File metadata and controls

314 lines (222 loc) · 17.3 KB

Instructions for Claude

Project Overview

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.

Tech Stack

  • 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-symbols icon 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

Project Structure

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

Tech Debt

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.

Development Guidelines

Browser Compatibility

  • 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.

CSS & Styling

  • 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/80, bg-primary/10) so that colours adapt to the active theme.
  • Secondary text uses text-muted (defined in src/app.css), never text-base-content at 60% opacity or less, and never text-muted with an opacity: those fail the 4.5:1 contrast ratio in the light theme. aria-hidden is no exemption, sighted users still read hidden text. Lower opacities are only allowed on aria-hidden icon components that duplicate adjacent text; icons that show a state (e.g. an expand chevron) need 3:1 and also use text-muted (enforced by npm run test:arch).
  • 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 @source directive for component CSS scanning

Accessibility (WCAG 2.2 AA)

The application must comply with WCAG 2.2 AA. This covers BITV 2.0 (German accessibility regulation, based on WCAG 2.1 AA). The axe tests in e2e/accessibility.spec.ts check the WCAG 2.0, 2.1 and 2.2 A/AA rules. 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, role where 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.
  • Target size: Interactive targets must be at least 24×24 CSS px, or have enough spacing to undersized neighbours (WCAG 2.5.8).
  • Form labels: Every form control must have a visible, associated <label> (see label-element association rule below).
  • Alt text: Every <img> must have a meaningful alt attribute (or alt="" for decorative images).
  • Language: The <html> element must have a lang attribute. Use lang attributes on content in other languages.
  • Screen reader support: Ensure content is announced correctly. Hide decorative elements with aria-hidden="true".

Logging

The project uses pino for structured JSON logging (server-side only).

Two usage patterns:

  1. Request-scoped (in +page.server.ts, +server.ts, hooks) — use the logger from event.locals:

    const log = event.locals.logger;
    log.info({ catalog_name: name }, 'Loading catalogue');
  2. 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

Monaco Editor

  • Always use dynamic imports to avoid SSR issues (import('monaco-editor'))
  • The editor must be loaded client-side only
  • Use the custom trinosql language registration for Trino SQL support

Updating the Trino SQL Grammar (ANTLR)

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.g4

Then regenerate the TypeScript lexer/parser:

npm run generate:antlr

After 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.).

Metrics

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.ts and 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.ts in client-side code or .svelte files
  • When to add metrics: New API endpoints, background operations, external service calls, and any operation where latency or error rates are operationally significant

Code Style & Best Practices

  • Use spaces not tabs

  • Run npm run format

  • Use British English

  • New Features: Do not add database migrations, data migrations, or legacy-data compatibility paths for a new feature unless persisted data or an external consumer already exists and requires it. Ask before adding one if that need is unclear.

  • 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.ts when adding unauthenticated pages. The /metrics endpoint is permanently public (Prometheus scraping) — never add auth in front of it.

  • Label-Element Association: Always explicitly associate <label> elements with their form controls using for and id attributes. 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 Modal component. Never use raw <dialog> elements directly. Control modals with a boolean $state and bind:open.

  • Date/Time Pickers: Always use a shared DateTimePicker component. Never use native <input type="date"> or <input type="datetime-local"> elements.

Internationalisation (i18n)

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.json and messages/de.json. Both files must always have the same keys.
  • Import pattern: import * as m from '$lib/paraglide/messages.js' — then use m.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 $derived so they update when the locale changes.
  • No hardcoded strings: Never hardcode user-visible strings in .svelte files. This includes text content, aria-label, title, placeholder, and alt attributes.
  • Adding new strings: Add the key to both messages/en.json and messages/de.json, then run npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide to regenerate typed message functions.

Commands

Node.js Version

The required Node.js version is pinned in .node-version for CI and .nvmrc for NVM. Keep both files synchronised. 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 .nvmrc
nvm use       # activates it in the current shell

Local Setup

For 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.

Development

npm run dev              # Start dev server

Building

npm run build            # Build for production
npm run preview          # Preview production build

Quality Checks (run after major changes)

npm run format               # Format code
pre-commit run --all-files   # Run all checks: lint (prettier + eslint + knip), type checking,
                             # markdownlint, yamllint, shellcheck, actionlint, hadolint, helm lint

Dead Code (knip)

npm run knip                 # Report unused files, exports, types and dependencies (part of npm run lint)
npm run knip:fix             # Remove unused exports and dependencies; review the diff and run npm install afterwards

knip fails on unused files, exports, types and dependencies. Delete dead code rather than silencing knip. For genuine false positives, configure knip.json: add files that nothing imports statically (CLI scripts, config files) to entry, and packages loaded at runtime by name (e.g. pino-pretty) or used only outside the source tree (e.g. in the Dockerfile) to ignoreDependencies, each with a comment explaining why.

Architecture Fitness Functions

npm run test:arch            # Run all architecture fitness function tests (~5 s)
npm run test:arch:report     # Same, plus generate HTML dependency/metrics reports in /reports/

Architecture tests live in src/architecture-tests/*.spec.ts and use ArchUnitTS together with plain Node.js fs checks. They run in CI and must remain green at all times.

Run npm run test:arch whenever you:

  • Add a new file to src/lib/server/ (verify it doesn't break client-boundary rules)
  • Add a new Svelte component (PascalCase naming and UI-pattern checks)
  • Add or remove message keys in messages/en.json (both locale files must stay in sync)
  • Refactor the Trino sub-layer (circular-dependency rules)

What the fitness functions enforce:

Category What is checked
Server / Client Boundary src/lib/client, stores, storage, editor, types must not import src/lib/server/**
No Circular Dependencies src/lib/** (excluding Trino) and src/routes/** must be cycle-free
Naming Conventions Stores → *.svelte.ts; lib components → PascalCase .svelte; server files → no .svelte.ts extension
Code Size Limits .ts files < 2 400 LOC; .svelte files < 1 100 LOC; test files < 1 000 LOC
UI Pattern Enforcement No native date inputs; <img> must have alt; no clickable <div> or <span> (with documented exceptions)
i18n Compliance messages/en.json and messages/de.json must have the same keys; no static aria-label="English text"

Extending the fitness functions: When you add a new architectural rule (e.g., a new layer, a new naming convention), add a new .spec.ts file in src/architecture-tests/ following the existing patterns. Use archunit for TypeScript dependency/cycle rules and plain Node.js fs for content checks on Svelte files.

E2E Testing

npm run test:e2e         # Run all E2E tests

E2E Testing Guidelines

E2E tests use Playwright and are located in e2e/.

When to Add Tests

  • 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:e2e after making UI changes to ensure nothing broke

When to Run Tests

  • Before committing UI changes
  • After modifying routes, layouts, or navigation
  • After changing form behaviour or validation
  • As part of quality checks before pushing

Writing Tests

import { test, expect } from '@playwright/test';

test('example test', async ({ page }) => {
  await page.goto('/some-page');
  await expect(page.locator('h1')).toContainText('Expected Title');
});

Authentication

Import test and expect from e2e/support/fixtures.ts, not @playwright/test. Its worker-scoped fixture logs each worker in via the mock OIDC server as its own user, so parallel workers never share server-side state. Opt out with test.use({ storageState: { cookies: [], origins: [] } }).

Important Notes

  • Environment: The app needs a .env file 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.