feat(m1-1): scaffold SvelteKit + Dockerfile + TDD test harness

- SvelteKit (JS, adapter-node) home page + /api/health route
- frontend/Dockerfile multi-stage build->serve (slabuploader-m1:local, 349MB)
- Test harness: Vitest unit + Playwright e2e; smoke tests RED->GREEN verified
- Build, image build, and image serve all verified
This commit is contained in:
Ty
2026-09-13 22:55:34 -07:00
parent ba8e5f72f6
commit 93666320ef
15 changed files with 2610 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
playwright-report
test-results
+18
View File
@@ -0,0 +1,18 @@
# SlabUploader M1 — frontend only (no FastAPI/Woo). Multi-stage: build then serve.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm install --omit=dev
COPY --from=build /app/build ./build
COPY --from=build /app/node_modules ./node_modules
EXPOSE 80
ENV PORT=80
CMD ["node", "build/index.js"]
+2405
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "slab-uploader-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test:unit": "vitest run",
"test:e2e": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/kit": "^2.21.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.1.0",
"typescript": "^5.8.0",
"vite": "^6.3.0",
"vitest": "^3.0.0"
},
"dependencies": {
"idb-keyval": "^6.2.1"
}
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig, devices } from '@playwright/test';
// Playwright config for M1 end-to-end tests.
// base URL points at the LAN UAT container (card M1-6). Override with BASE_URL.
export default defineConfig({
testDir: './tests/e2e',
timeout: 60_000,
use: {
baseURL: process.env.BASE_URL || 'http://192.168.1.202:18080'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
});
+32
View File
@@ -0,0 +1,32 @@
:root {
--bg: #1a1a1a;
--fg: #f2f2f2;
--accent: #4f9d69;
--checker: repeating-conic-gradient(#e8e8e8 0% 25%, #f7f7f7 0% 50%) 0 0 / 24px 24px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--fg);
}
button {
font: inherit;
padding: 0.6rem 1rem;
border: 0;
border-radius: 8px;
background: var(--accent);
color: #04140b;
font-weight: 600;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<script>
import '../app.css';
</script>
<svelte:head>
<title>SlabUploader — Milestone 1</title>
</svelte:head>
<main>
<h1>SlabUploader</h1>
<p>Milestone 1 — Capture (browser-only)</p>
<slot />
</main>
+12
View File
@@ -0,0 +1,12 @@
<script>
/** M1 placeholder home — the 5-state capture flow is built in later cards. */
</script>
<div class="capture">
<h2>Capture</h2>
<p>The M1 capture flow (Add photo → Mask → Length axis → Measure → PNG) lands in cards M1-3…M1-5.</p>
</div>
<style>
.capture { max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
</style>
@@ -0,0 +1,6 @@
// Minimal health route so the UAT container has a non-SPA probe target.
import { json } from '@sveltejs/kit';
export function GET() {
return json({ ok: true, milestone: 1, app: 'slab-uploader' });
}
+10
View File
@@ -0,0 +1,10 @@
import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
}
};
export default config;
+12
View File
@@ -0,0 +1,12 @@
import { test, expect } from '@playwright/test';
// E2E smoke: the app serves and the health route responds.
// Later cards extend this to walk the full 5-state capture flow.
test('M1-1 harness smoke: app serves + health ok', async ({ page, request }) => {
await page.goto('/');
await expect(page).toHaveTitle(/SlabUploader/);
const res = await request.get('/api/health');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.ok).toBe(true);
});
+10
View File
@@ -0,0 +1,10 @@
import { describe, it, expect } from 'vitest';
// Smoke test for the M1-1 harness: proves the JS unit runner works end-to-end.
// This is deliberately trivial — later cards (M1-2+) replace it with the
// deterministic-core suite. It must pass to certify the harness is GREEN.
describe('M1 harness smoke', () => {
it('adds numbers (harness is live)', () => {
expect(1 + 1).toBe(2);
});
});
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": false,
"moduleResolution": "bundler"
}
}
+11
View File
@@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
test: {
environment: 'node',
include: ['tests/unit/**/*.test.js'],
exclude: ['tests/e2e/**', 'node_modules/**', 'build/**']
}
});