# SolidHaus — Claude Code Implementation Guide This document provides step-by-step implementation instructions for Claude Code. ## Overview - **Framework**: SvelteKit 2 (Svelte 5 with runes) + Capacitor - **Goal**: Local-first, multi-user household inventory app with barcode scanning - **Key deps**: idb, nanoid, bwip-js, @capacitor-mlkit/barcode-scanning, automerge, @inrupt/solid-client ## Priority: Phase 1 — Core MVP Build the app incrementally. Each task builds on the previous. --- ## Task 1: Project Scaffold Create a SvelteKit project with Capacitor and Tailwind CSS. ```bash npx sv create solidhaus cd solidhaus # Static adapter for Capacitor npm install -D @sveltejs/adapter-static # Capacitor npm install @capacitor/core @capacitor/cli npx cap init solidhaus so.toph.solidhaus --web-dir build npm install @capacitor/android @capacitor/ios npx cap add android npx cap add ios # Barcode scanner npm install @capacitor-mlkit/barcode-scanning # Core deps npm install idb nanoid date-fns bwip-js jspdf # Solid (Phase 3, but install now) npm install @inrupt/solid-client @inrupt/solid-client-authn-browser @inrupt/vocab-common-rdf # Automerge (Phase 2, but install now) npm install @automerge/automerge @automerge/automerge-repo @automerge/automerge-repo-network-websocket @automerge/automerge-repo-storage-indexeddb # Dev npm install -D tailwindcss @tailwindcss/vite ``` ### svelte.config.js: ```javascript import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; export default { preprocess: vitePreprocess(), kit: { adapter: adapter({ fallback: 'index.html' // SPA mode for Capacitor }) } }; ``` ### src/routes/+layout.ts: ```typescript export const ssr = false; export const prerender = false; ``` ### vite.config.ts: ```typescript import { sveltekit } from '@sveltejs/kit/vite'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tailwindcss(), sveltekit(), ], }); ``` ### capacitor.config.ts: ```typescript import type { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'so.toph.solidhaus', appName: 'SolidHaus', webDir: 'build', server: { androidScheme: 'https' } }; export default config; ``` ### File structure: Follow the project structure defined in PROJECT_SPECIFICATION.md §12. --- ## Task 2: Data Layer (IndexedDB) Create `src/lib/data/db.ts` using the `idb` library. ### Database Schema: ```typescript import { openDB, DBSchema } from 'idb'; interface SolidHausDB extends DBSchema { items: { key: string; // shortId value: { shortId: string; // 7-char nanoid name: string; description: string; category: string; brand: string; serialNumber: string; color: string; purchaseDate: string | null; // Item type itemType: 'durable' | 'consumable' | 'disposable' | 'perishable' | 'media' | 'clothing' | 'document' | 'container'; // Quantity (consumables/perishables) currentQuantity: number | null; originalQuantity: number | null; quantityUnit: string | null; lowThreshold: number | null; expiryDate: string | null; // Barcode barcodeFormat: 'qr' | 'datamatrix' | 'code128'; barcodeUri: string; photoIds: string[]; // Location tracking lastSeenAt: string | null; lastSeenTimestamp: string | null; lastUsedAt: string | null; supposedToBeAt: string | null; locationConfidence: 'confirmed' | 'likely' | 'assumed' | 'unknown'; // Custody state custodyState: 'checked-in' | 'checked-out'; checkedOutSince: string | null; checkedOutReason: 'in-use' | 'in-transit' | 'lent' | 'in-repair' | 'temporary' | 'consumed' | null; checkedOutFrom: string | null; checkedOutTo: string | null; checkedOutNote: string | null; // Storage tier storageTier: 'hot' | 'warm' | 'cold'; storageContainerId: string | null; storageContainerLabel: string | null; // Label labelPrinted: boolean; labelPrintedAt: string | null; labelBatchId: string | null; // Metadata createdAt: string; updatedAt: string; tags: string[]; createdBy: string | null; }; indexes: { 'by-name': string; 'by-category': string; 'by-location': string; 'by-type': string; 'by-custody': string; }; }; locations: { key: string; value: { id: string; name: string; description: string; parentId: string | null; locationType: 'house' | 'floor' | 'room' | 'furniture' | 'shelf' | 'drawer' | 'box' | 'wall' | 'outdoor'; defaultStorageTier: 'hot' | 'warm' | 'cold' | null; sortOrder: number; createdAt: string; updatedAt: string; }; indexes: { 'by-parent': string; 'by-name': string; }; }; sightings: { key: string; value: { id: string; itemId: string; locationId: string; timestamp: string; sightingType: 'scan' | 'manual' | 'camera-detect' | 'audit-verify'; confidence: 'confirmed' | 'inferred' | 'assumed'; notes: string; createdBy: string | null; }; indexes: { 'by-item': string; 'by-location': string; 'by-timestamp': string; }; }; photos: { key: string; value: { id: string; itemId: string; blob: Blob; thumbnail: Blob | null; createdAt: string; }; indexes: { 'by-item': string; }; }; preGeneratedIds: { key: string; value: { id: string; generatedAt: string; assignedTo: string | null; batchId: string; }; indexes: { 'by-batch': string; 'by-assigned': string; }; }; settings: { key: string; value: { key: string; value: unknown; }; }; } const DB_NAME = 'solidhaus'; const DB_VERSION = 1; export async function getDB() { return openDB(DB_NAME, DB_VERSION, { upgrade(db) { // Items const itemStore = db.createObjectStore('items', { keyPath: 'shortId' }); itemStore.createIndex('by-name', 'name'); itemStore.createIndex('by-category', 'category'); itemStore.createIndex('by-location', 'lastSeenAt'); itemStore.createIndex('by-type', 'itemType'); itemStore.createIndex('by-custody', 'custodyState'); // Locations const locStore = db.createObjectStore('locations', { keyPath: 'id' }); locStore.createIndex('by-parent', 'parentId'); locStore.createIndex('by-name', 'name'); // Sightings const sightStore = db.createObjectStore('sightings', { keyPath: 'id' }); sightStore.createIndex('by-item', 'itemId'); sightStore.createIndex('by-location', 'locationId'); sightStore.createIndex('by-timestamp', 'timestamp'); // Photos const photoStore = db.createObjectStore('photos', { keyPath: 'id' }); photoStore.createIndex('by-item', 'itemId'); // Pre-generated IDs const pregenStore = db.createObjectStore('preGeneratedIds', { keyPath: 'id' }); pregenStore.createIndex('by-batch', 'batchId'); pregenStore.createIndex('by-assigned', 'assignedTo'); // Settings db.createObjectStore('settings', { keyPath: 'key' }); }, }); } ``` ### CRUD Functions: Create `src/lib/data/items.ts`, `locations.ts`, `sightings.ts`, `labels.ts` with standard CRUD: - `getAll()`, `getById()`, `create()`, `update()`, `remove()` - Each function gets the DB instance via `getDB()` and performs the operation. ### ID Generator: ```typescript // src/lib/utils/id.ts import { customAlphabet } from 'nanoid'; const ALPHABET = '23456789abcdefghjkmnpqrstuvwxyz'; export const generateItemId = customAlphabet(ALPHABET, 7); export const generateSightingId = () => `s_${generateItemId()}`; ``` --- ## Task 3: Barcode Scanner Component Create `src/lib/components/Scanner.svelte`. ### Requirements: - On native (Capacitor): use @capacitor-mlkit/barcode-scanning - On web: use Barcode Detection API with ZXing fallback - Detect QR Code, Code 128, DataMatrix, EAN-13 - Parse detected value through `parseItemId()` - Haptic feedback on successful scan (Capacitor Haptics) ### Implementation approach: ```typescript // src/lib/scanning/detector.ts import { Capacitor } from '@capacitor/core'; import { BarcodeScanner, BarcodeFormat } from '@capacitor-mlkit/barcode-scanning'; import { parseItemId } from './parser'; export async function scanBarcode(): Promise { if (Capacitor.isNativePlatform()) { // Native: use ML Kit const { barcodes } = await BarcodeScanner.scan({ formats: [BarcodeFormat.QrCode, BarcodeFormat.Code128, BarcodeFormat.DataMatrix], }); for (const barcode of barcodes) { const id = parseItemId(barcode.rawValue); if (id) return id; } return null; } else { // Web: use Barcode Detection API return scanWithWebAPI(); } } async function scanWithWebAPI(): Promise { if (!('BarcodeDetector' in window)) { // Load ZXing fallback // Use html5-qrcode library return null; } const detector = new BarcodeDetector({ formats: ['qr_code', 'code_128', 'data_matrix'], }); // Get camera stream const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); // Create video element, detect from frames // ... (continuous detection loop at ~10fps) return null; } ``` ### ID Parsing: ```typescript // src/lib/scanning/parser.ts const VALID_CHARS = /^[23456789a-hjkmnp-z]{7}$/; export function parseItemId(rawValue: string): string | null { // HTTPS URI const httpsMatch = rawValue.match(/haus\.toph\.so\/([23456789a-hjkmnp-z]{7})$/); if (httpsMatch) return httpsMatch[1]; // Custom scheme const schemeMatch = rawValue.match(/^haus:\/\/([23456789a-hjkmnp-z]{7})$/); if (schemeMatch) return schemeMatch[1]; // Raw ID if (VALID_CHARS.test(rawValue)) return rawValue; return null; } ``` --- ## Task 4: Item Management ### ItemForm Component (`src/lib/components/ItemForm.svelte`) Fields: name, description, category, brand, serialNumber, color, purchaseDate, itemType, location, storageTier. For consumable/perishable types, show additional fields: currentQuantity, originalQuantity, quantityUnit, lowThreshold, expiryDate. Auto-generate shortId on create (or pre-fill if associating a pre-printed label). ### ItemDetail Component (`src/lib/components/ItemCard.svelte`) Show: name, category, custody badge, location, confidence badge, storage tier, sighting history. For consumables: quantity bar with percentage. Actions: Check In/Out (contextual), Edit, Print Label, View History. ### ItemList Component (`src/routes/items/+page.svelte`) Filterable list: search, category filter, custody state filter, type filter. Sort by name, last seen, custody state. --- ## Task 5: Location Management ### LocationTree Component (`src/lib/components/LocationTree.svelte`) Collapsible tree view using `parentId` relationships. Show item count per location. Tap to browse items in that location. ### LocationPicker Component (`src/lib/components/LocationPicker.svelte`) Used in forms and scan flows. Shows tree with radio selection. Quick-access "recent locations" at top. ### Default Locations: Pre-populate on first run: ```typescript const DEFAULT_LOCATIONS = [ { id: 'home', name: 'Home', parentId: null, type: 'house' }, { id: 'eg', name: 'Erdgeschoss', parentId: 'home', type: 'floor' }, { id: 'og', name: 'Obergeschoss', parentId: 'home', type: 'floor' }, { id: 'keller', name: 'Keller', parentId: 'home', type: 'floor' }, { id: 'kueche', name: 'Küche', parentId: 'eg', type: 'room' }, { id: 'wohnzimmer', name: 'Wohnzimmer', parentId: 'eg', type: 'room' }, { id: 'flur', name: 'Flur', parentId: 'eg', type: 'room' }, { id: 'schlafzimmer', name: 'Schlafzimmer', parentId: 'og', type: 'room' }, { id: 'buero', name: 'Büro', parentId: 'og', type: 'room' }, { id: 'bad', name: 'Bad', parentId: 'og', type: 'room' }, { id: 'werkstatt', name: 'Werkstatt', parentId: 'keller', type: 'room' }, ]; ``` --- ## Task 6: Label Sheet Generation ### Label Sheet PDF (`src/lib/printing/labelSheet.ts`) Generate A4 PDF of QR code stickers: ```typescript import bwipjs from 'bwip-js'; import jsPDF from 'jspdf'; export async function generateLabelSheetPDF(ids: string[]): Promise { const doc = new jsPDF('p', 'mm', 'a4'); const COLS = 5; const ROWS = 10; const CELL_W = 38; // mm const CELL_H = 27; // mm const MARGIN_X = 8; const MARGIN_Y = 10; const QR_SIZE = 18; // mm for (let i = 0; i < ids.length; i++) { const col = i % COLS; const row = Math.floor(i / COLS) % ROWS; const page = Math.floor(i / (COLS * ROWS)); if (i > 0 && i % (COLS * ROWS) === 0) { doc.addPage(); } const x = MARGIN_X + col * CELL_W; const y = MARGIN_Y + row * CELL_H; // Generate QR code as PNG const canvas = document.createElement('canvas'); bwipjs.toCanvas(canvas, { bcid: 'qrcode', text: `https://haus.toph.so/${ids[i]}`, scale: 3, includetext: false, }); const qrDataUrl = canvas.toDataURL('image/png'); doc.addImage(qrDataUrl, 'PNG', x + 1, y + 1, QR_SIZE, QR_SIZE); // ID text doc.setFontSize(8); doc.setFont('courier', 'normal'); doc.text(ids[i], x + QR_SIZE + 2, y + QR_SIZE / 2 + 1); } return doc.output('blob'); } ``` ### Print Server Client (`src/lib/printing/printServer.ts`) ```typescript export interface PrintServerConfig { baseUrl: string; // e.g., "http://printer.local:3030" } export async function printLabels( config: PrintServerConfig, labels: { id: string; uri: string }[], format: 'sheet' | 'strip' = 'sheet', ): Promise { const response = await fetch(`${config.baseUrl}/api/print`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ labels, format }), }); if (!response.ok) { throw new Error(`Print failed: ${response.statusText}`); } } ``` --- ## Task 7: App Shell & Navigation ### Layout (`src/routes/+layout.svelte`) ```svelte
{@render children()}
``` ### BottomNav (`src/lib/components/BottomNav.svelte`) ```svelte ``` --- ## Task 8: State Management (Svelte 5 Runes) Use Svelte 5 runes for reactive state. Create stores in `src/lib/stores/`: ```typescript // src/lib/stores/inventory.svelte.ts import { getDB } from '$lib/data/db'; class InventoryStore { items = $state([]); locations = $state([]); loading = $state(true); checkedOutItems = $derived( this.items.filter(i => i.custodyState === 'checked-out') ); overdueItems = $derived( this.items.filter(i => i.custodyState === 'checked-out' && i.checkedOutSince && Date.now() - new Date(i.checkedOutSince).getTime() > 7 * 86400000 ) ); lowStockItems = $derived( this.items.filter(i => i.currentQuantity != null && i.lowThreshold != null && i.currentQuantity <= i.lowThreshold ) ); async loadAll() { const db = await getDB(); this.items = await db.getAll('items'); this.locations = await db.getAll('locations'); this.loading = false; } async createItem(item: Item) { const db = await getDB(); await db.put('items', item); this.items = [...this.items, item]; } async updateItem(shortId: string, updates: Partial) { const db = await getDB(); const existing = await db.get('items', shortId); if (!existing) return; const updated = { ...existing, ...updates, updatedAt: new Date().toISOString() }; await db.put('items', updated); this.items = this.items.map(i => i.shortId === shortId ? updated : i); } } export const inventory = new InventoryStore(); ``` --- ## Design Guidelines ### Color Palette (Dark theme, slate-based): - `bg-slate-900` — main background - `bg-slate-800` — cards, surfaces - `bg-slate-700` — hover, borders - `text-blue-400` — primary actions - `text-emerald-400` — confirmed, checked in - `text-amber-400` — likely, warning - `text-red-400` — unknown, danger ### Confidence Badges: - Confirmed → `bg-emerald-500/20 text-emerald-400` + ✓ - Likely → `bg-amber-500/20 text-amber-400` + ◷ - Assumed → `bg-slate-500/20 text-slate-400` + ? - Unknown → `bg-red-500/20 text-red-400` + ! ### Mobile Considerations: - Touch targets: min 44×44px - Bottom sheet modals for scan results - Haptic feedback on scan - Skeleton loading states - Monospace font for IDs: `font-mono text-lg tracking-wider` --- ## Testing Approach ### Unit Tests: - ID generation (uniqueness, format validation) - ID parsing (HTTPS, haus://, raw) - Confidence decay logic - Quantity formatting ### Integration Tests: - IndexedDB CRUD operations - Check-in/check-out state transitions - Pre-generated ID → item association ### E2E Tests (Playwright): - Create item flow - Scan → check out → check in cycle - Generate label sheet - Search and filter --- ## Non-Functional Requirements - First paint < 1s on mobile - Scanner frame processing ≥ 10fps - IndexedDB operations < 50ms - Full offline capability (all features work without network) - Multi-device sync via Automerge (eventual consistency) - Accessible (ARIA labels, screen reader support) --- ## Key Algorithms ### Confidence Decay ```typescript export function getConfidence(lastSeenTimestamp: string | null): ConfidenceLevel { if (!lastSeenTimestamp) return 'unknown'; const daysSince = differenceInDays(new Date(), new Date(lastSeenTimestamp)); if (daysSince <= 30) return 'confirmed'; if (daysSince <= 90) return 'likely'; if (daysSince <= 180) return 'assumed'; return 'unknown'; } ``` ### Sighting Processing ```typescript async function processSighting(itemId: string, locationId: string, type: SightingMethod) { const now = new Date().toISOString(); await createSighting({ id: generateSightingId(), itemId, locationId, timestamp: now, sightingType: type, confidence: 'confirmed', notes: '' }); await updateItem(itemId, { lastSeenAt: locationId, lastSeenTimestamp: now, locationConfidence: 'confirmed', updatedAt: now }); } ``` ### Check-Out Processing ```typescript async function processCheckOut(itemId: string, reason: CheckOutReason, fromLocationId: string, toLocationId: string | null, note = '') { const now = new Date().toISOString(); await updateItem(itemId, { custodyState: 'checked-out', checkedOutSince: now, checkedOutReason: reason, checkedOutFrom: fromLocationId, checkedOutTo: toLocationId, checkedOutNote: note, lastSeenAt: toLocationId ?? fromLocationId, lastSeenTimestamp: now, locationConfidence: 'confirmed', }); } ``` ### Check-In Processing ```typescript async function processCheckIn(itemId: string, returnToLocationId: string) { const now = new Date().toISOString(); await updateItem(itemId, { custodyState: 'checked-in', checkedOutSince: null, checkedOutReason: null, checkedOutFrom: null, checkedOutTo: null, checkedOutNote: null, lastSeenAt: returnToLocationId, lastSeenTimestamp: now, locationConfidence: 'confirmed', supposedToBeAt: returnToLocationId, }); } ``` ### Dashboard Queries ```typescript function getCheckedOutItems(): Item[] { return allItems.filter(i => i.custodyState === 'checked-out'); } function getOverdueItems(maxDays = 7): Item[] { const cutoff = subDays(new Date(), maxDays); return allItems.filter(i => i.custodyState === 'checked-out' && i.checkedOutSince && new Date(i.checkedOutSince) < cutoff); } function getLentItems(): Item[] { return allItems.filter(i => i.custodyState === 'checked-out' && i.checkedOutReason === 'lent'); } function getItemsInContainer(containerId: string): Item[] { return allItems.filter(i => i.storageContainerId === containerId); } ``` --- ## Forgejo PR Workflow Every substantive session is tracked in `.claude/request-log.jsonl` and backed by a Forgejo PR. ### When to create a branch + PR Create a branch and PR for: - New features (`feat/`) - Bug fixes (`fix/`) - Refactors (`refactor/`) - Dependency or config changes (`chore/`) Skip branching/PR for: - Questions, explanations, code reading - Trivial one-liner fixes that are best committed directly to the current branch ### Branch naming ``` feat/YYYYMMDD-short-kebab-description fix/YYYYMMDD-short-kebab-description chore/YYYYMMDD-short-kebab-description refactor/YYYYMMDD-short-kebab-description ``` Check the current branch first: ```bash git rev-parse --abbrev-ref HEAD ``` If already on a feature branch from this session, continue on it. Only create a new branch for a new distinct task. ### Workflow for each task 1. **Create branch** (if needed): ```bash git checkout -b feat/$(date +%Y%m%d)-short-description ``` 2. **Do the work** — write code, run tests, iterate. 3. **Commit** with conventional commit messages: ```bash git add git commit -m "feat: short description" ``` 4. **Push**: ```bash git push -u origin HEAD ``` 5. **Create PR**: ```bash bash .claude/scripts/create-pr.sh \ "feat: short description" \ "$(cat <<'EOF' ## Summary - What was done and why ## Test plan - [ ] Manual test steps 🤖 Generated with Claude Code EOF )" ``` This also updates `.claude/request-log.jsonl` with the PR number. 6. **Commit the updated request log** (as part of the PR or separately): ```bash git add .claude/request-log.jsonl git commit -m "chore: log request #" ``` ### Show history When the user asks for history, recent work, or what's been done: ```bash bash .claude/scripts/show-history.sh bash .claude/scripts/show-history.sh --filter open bash .claude/scripts/show-history.sh --last 10 ``` ### tea CLI quick reference ```bash tea pr list # list open PRs tea pr view # view a PR tea pr merge # merge a PR tea issue create # create an issue tea repo info # show repo details tea login list # show configured Forgejo instances ``` ### Request log format `.claude/request-log.jsonl` — one JSON object per line, git-tracked: ```json { "id": "req_20260226_143012_a3f9b", "timestamp": "2026-02-26T14:30:12Z", "prompt": "Add dark mode support", "session": "abc123", "branch": "feat/20260226-dark-mode", "pr_number": 42, "pr_url": "https://forge.example.com/user/repo/pulls/42", "status": "open" } ``` Possible statuses: `pending` (no PR yet), `open`, `merged`, `closed`.