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:
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
playwright-report
|
||||
test-results
|
||||
@@ -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"]
|
||||
Generated
+2405
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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'] }
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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/**']
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user