feat(web): add eslint + prettier + vitest toolchain + web CI job (R8)

Added to web/package.json devDeps: eslint (9, flat config) +
eslint-plugin-svelte + typescript-eslint + globals; prettier +
prettier-plugin-svelte; vitest (jsdom env) + jsdom. New scripts: lint,
lint:fix, format, format:check, test, test:watch.

Configs:
- web/eslint.config.js — flat config, TS + Svelte, browser/node globals,
  no-explicit-any as warn, unused-vars as error (ignores _-prefixed).
- web/.prettierrc.json — single-quote, 100 width, svelte parser override.
- web/.prettierignore — dist/node_modules/build/lockfiles.
- web/vite.config.ts — vitest test block via reference directive, jsdom env,
  globals enabled.

Sample test: web/src/lib/utils.test.ts (6 tests covering relativeTime,
truncateMiddle, debounce — all passing).

CI: new web job in .gitea/workflows/ci.yml (npm ci, check [advisory],
lint [advisory], format:check [advisory], test [gate], build [gate]).
Advisory steps use continue-on-error until the baseline is clean —
matching the existing golangci-lint advisory pattern.

Known baseline surfaced by the new toolchain (pre-existing, not caused
by R8): svelte-check 154 errors (133-file config cascade), eslint 126
errors + 12 warnings (unused vars, @html XSS, unused CSS), prettier 175
unformatted files. Fixing these is a follow-up cleanup.

VERSION 0.7.9 -> 0.7.10. Plan R8 marked done; C.1 updated.
This commit is contained in:
2026-07-17 22:49:27 +02:00
parent fb39a48bef
commit 463bdacf5c
10 changed files with 4090 additions and 66 deletions

View File

@@ -70,3 +70,30 @@ jobs:
- uses: actions/checkout@v4
- name: docker build (verify image builds; no push)
run: docker build -f compose/oikos/Dockerfile -t oikos:ci .
web:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci
- name: svelte-check (advisory — baseline not yet clean)
run: npm run check
continue-on-error: true
- name: eslint (advisory — baseline not yet clean)
run: npm run lint
continue-on-error: true
- name: prettier format check (advisory — baseline not yet clean)
run: npm run format:check
continue-on-error: true
- name: test
run: npm run test
- name: build
run: npm run build

View File

@@ -1 +1 @@
0.7.9
0.7.10

View File

@@ -176,14 +176,29 @@ CONTRIBUTING §Testing gates: policy + learning ≥ 80%, others ≥ 60%.
`npm run build` succeeds with Svelte 5 warnings. 722 KB JS bundle (222 KB
gzip), no code splitting.
### C.1 Tooling gaps — fixed in this pass
### C.1 Tooling gaps — fixed in this pass + R8
- `package.json` had only `dev`/`build`/`preview`. **Added** `check`
(`svelte-check`), `typecheck` (`tsc --noEmit`), and `lint` scripts, plus
`svelte-check` + `typescript` devDeps. Run `npm install` to pick them up.
- Still missing: `eslint` + `eslint-plugin-svelte` + `prettier`, and any test
runner (`vitest`). Recommended next step.
- No CI config in `web/` — nothing enforces type-check/build on PR.
- **R8 added:** `eslint` (flat config) + `eslint-plugin-svelte` +
`typescript-eslint` + `globals`; `prettier` + `prettier-plugin-svelte`;
`vitest` (jsdom env) with a sample test (`src/lib/utils.test.ts`, 6 tests).
New scripts: `lint`, `lint:fix`, `format`, `format:check`, `test`,
`test:watch`. Vitest config wired into `vite.config.ts` via
`/// <reference types="vitest/config" />`.
- **CI:** new `web` job in `.gitea/workflows/ci.yml` runs `npm ci`,
`npm run check` (advisory), `npm run lint` (advisory),
`npm run format:check` (advisory), `npm run test` (gate),
`npm run build` (gate). Advisory steps use `continue-on-error: true`
until the lint/check baseline is clean — matching the existing
`golangci-lint` advisory pattern.
- **Known baseline:** svelte-check reports 154 pre-existing errors (133
files, "No Svelte configuration found in vite config" cascade — not
caused by R8); eslint reports 126 errors + 12 warnings (unused vars,
`@html` XSS, unused CSS); prettier reports 175 unformatted files. These
are real findings surfaced by the new toolchain — fixing them is a
follow-up cleanup task.
### C.2 Dead code — the tool-renderer registry (21 files, ~1.5k lines)
@@ -414,7 +429,7 @@ the gitignore comment); `build` target ensures `bin/` exists; `clean` removes `b
| R5 | Rewrite `.agents/domains/knowledge/schema.md` + `.agents/shared/llm-wiki.md` for the DB-native model; delete/deprecate root `inventory.yaml` | M | Low | ✅ done |
| R6 | Inject desktop `version` from `VERSION` via ldflags; fix `Makefile` `BINARY` colliding with `oikos/` dir | S | Low | ✅ done |
| R7 | Add tests for `learning` (80% gate), `actuator`, `scheduler`, `domain`, `notifier`, `knowledge` | L | Low |
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low |
| R8 | Add `eslint`+`prettier`+`vitest` to `web/`; wire `svelte-check`+`tsc` into CI; add `web/` CI job | M | Low | ✅ done |
| R9 | Define `OikosEvent` discriminated union; eliminate ~15 `any` sites in web | S | Low |
| R10 | Replace `<svelte:component>` in `ActivityTimeline.svelte:103`; fix `state_referenced_locally` warnings | S | Low |
| R11 | Add the 8 manually-registered `serve*` routes to `openapi.yaml` (or document the carve-out) | S | Low |

4
web/.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
dist/
node_modules/
build/
package-lock.json

9
web/.prettierrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"useTabs": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

39
web/eslint.config.js Normal file
View File

@@ -0,0 +1,39 @@
import js from '@eslint/js'
import ts from 'typescript-eslint'
import svelte from 'eslint-plugin-svelte'
import globals from 'globals'
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
{
files: ['**/*.{ts,js,svelte}'],
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['dist/', 'node_modules/', 'build/', '*.config.{ts,js}']
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
]
}
}
)

3971
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,12 @@
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"typecheck": "tsc --noEmit",
"lint": "svelte-check --tsconfig ./tsconfig.json"
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"@lucide/svelte": "^1.23.0",
@@ -18,13 +23,21 @@
"@tsconfig/svelte": "^5.0.0",
"@types/d3-force": "^3.0.10",
"bits-ui": "^2.18.1",
"eslint": "^9.0.0",
"eslint-plugin-svelte": "^2.46.0",
"globals": "^15.0.0",
"jsdom": "^25.0.0",
"prettier": "^3.3.0",
"prettier-plugin-svelte": "^3.3.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.1.1",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.2",
"typescript": "^5.5.0",
"vite": "^6.0.0"
"typescript-eslint": "^8.0.0",
"vite": "^6.0.0",
"vitest": "^2.0.0"
},
"dependencies": {
"clsx": "^2.1.1",

54
web/src/lib/utils.test.ts Normal file
View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest'
import { relativeTime, truncateMiddle, debounce } from './utils'
describe('relativeTime', () => {
it('returns "never" for null/undefined/empty', () => {
expect(relativeTime(null)).toBe('never')
expect(relativeTime(undefined)).toBe('never')
expect(relativeTime('')).toBe('never')
})
it('returns "just now" for future timestamps', () => {
const future = new Date(Date.now() + 10_000).toISOString()
expect(relativeTime(future)).toBe('just now')
})
it('formats seconds/minutes/hours/days', () => {
const now = Date.now()
expect(relativeTime(new Date(now - 5_000).toISOString())).toBe('5s ago')
expect(relativeTime(new Date(now - 120_000).toISOString())).toBe('2m ago')
expect(relativeTime(new Date(now - 3_600_000).toISOString())).toBe('1h ago')
expect(relativeTime(new Date(now - 86_400_000 * 2).toISOString())).toBe('2d ago')
})
})
describe('truncateMiddle', () => {
it('returns the string unchanged when at or under maxLen', () => {
expect(truncateMiddle('short', 36)).toBe('short')
expect(truncateMiddle('exactly36chars_exactly36chars_xxxxx', 36)).toBe(
'exactly36chars_exactly36chars_xxxxx'
)
})
it('truncates in the middle, preserving both ends', () => {
const out = truncateMiddle('investigation:nomos/dragonfly-memlock-overrun', 20)
expect(out).toContain('…')
// keep = floor((20 - 1) / 2) = 9 chars from each end
expect(out.startsWith('investiga')).toBe(true)
expect(out.endsWith('-overrun')).toBe(true)
expect(out.length).toBeLessThanOrEqual(20)
})
})
describe('debounce', () => {
it('collapses rapid calls into one trailing invocation', async () => {
let calls = 0
const fn = debounce(() => calls++, 20)
fn()
fn()
fn()
expect(calls).toBe(0)
await new Promise((r) => setTimeout(r, 40))
expect(calls).toBe(1)
})
})

View File

@@ -1,7 +1,8 @@
/// <reference types="vitest/config" />
import { svelte } from '@sveltejs/vite-plugin-svelte'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { readFileSync, existsSync } from 'fs'
import { defineConfig } from 'vite'
// In Docker, VERSION is copied into the build WORKDIR (/build/web/VERSION).
// In local dev, the cwd is web/ and VERSION is two dirs up (../../VERSION
@@ -50,5 +51,10 @@ export default defineConfig({
// here so dev and prod agree on nomos's actual route paths.
'/agent': authProxy('http://localhost:8092', (path) => path.replace(/^\/agent/, ''))
}
},
test: {
environment: 'jsdom',
globals: true,
include: ['src/**/*.{test,spec}.{ts,js}']
}
})