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