From cbb77f1b57826f248b9690a1d2bba8637da33367 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sun, 24 May 2026 18:52:54 +0000 Subject: [PATCH] chore: remove unused files --- PLAN.md | 831 -------------------------------------------------------- 1 file changed, 831 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 4220717..0000000 --- a/PLAN.md +++ /dev/null @@ -1,831 +0,0 @@ -# Project: `excalidraw-box` - -## Goal - -Build a small self-hosted web app that provides: - -```text -- Excalidraw editor -- Server-side autosave to SQLite -- No collaboration -- No accounts inside the app -- Reverse-proxy auth handled by Caddy -- Manual export to PNG/SVG/.excalidraw -- Docker Compose deployment -``` - -The app should be intentionally small and auditable. - ---- - -# Non-goals - -Do **not** implement: - -```text -- real-time collaboration -- public sharing links -- user accounts -- teams/workspaces -- comments -- Firebase -- S3/MinIO in v1 -- complex permissions -``` - -Authentication is handled outside the app by Caddy `basic_auth` or later Authelia/AuthentiK. - ---- - -# Stack - -Use: - -```text -Next.js App Router -React -TypeScript -@excalidraw/excalidraw -better-sqlite3 -Docker Compose -Caddy reverse proxy -``` - -Rationale: - -- `@excalidraw/excalidraw` is the official embeddable package. ([npm][1]) -- Excalidraw exposes `onChange(elements, appState, files)` for persistence. ([Excalidraw Docs][2]) -- Excalidraw provides export utilities such as `exportToCanvas` for rendering scenes to images. ([Excalidraw Docs][3]) -- Next.js officially supports standalone Docker deployment. ([Next.js][4]) - ---- - -# Version 1 feature set - -## V1 must have - -```text -1. Single default drawing -2. Load drawing from SQLite on page load -3. Autosave drawing to SQLite after edits -4. Debounce saves by 1.5 seconds -5. Show save status: - - Saved - - Saving… - - Save failed -6. Export PNG from the current scene -7. Export .excalidraw JSON from the current scene -8. Persist SQLite database under /data -9. Dockerfile -10. docker-compose.yml -11. Caddyfile example -``` - -## V1 should avoid - -```text -- multiple drawings -- login UI -- sharing -- collaboration -- migrations framework -- Prisma, unless really needed -``` - -Use plain `better-sqlite3` to keep the code small. - ---- - -# Directory structure - -```text -excalidraw-autosave/ - app/ - api/ - drawing/ - route.ts - globals.css - layout.tsx - page.tsx - - components/ - ExcalidrawClient.tsx - - lib/ - db.ts - scene.ts - debounce.ts - - public/ - - Dockerfile - docker-compose.yml - Caddyfile.example - next.config.ts - package.json - tsconfig.json - .dockerignore - README.md -``` - ---- - -# Data model - -Use one SQLite table. - -```sql -CREATE TABLE IF NOT EXISTS drawings ( - id TEXT PRIMARY KEY, - data TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - -For v1, always use: - -```text -id = "default" -``` - -Stored `data` should be the scene object: - -```json -{ - "type": "excalidraw-autosave-scene", - "version": 1, - "elements": [], - "appState": {}, - "files": {} -} -``` - -Do not store only rendered PNG. Store the editable scene JSON as the source of truth. - ---- - -# API design - -## `GET /api/drawing` - -Returns the saved drawing. - -If no drawing exists, return an empty scene: - -```json -{ - "type": "excalidraw-autosave-scene", - "version": 1, - "elements": [], - "appState": {}, - "files": {} -} -``` - -## `POST /api/drawing` - -Accepts: - -```json -{ - "elements": [], - "appState": {}, - "files": {} -} -``` - -Behavior: - -```text -- Validate body is an object. -- Validate elements is an array. -- Validate appState is an object. -- Validate files is an object. -- Strip volatile or UI-only appState fields if needed. -- Save JSON to SQLite. -- Upsert row id = "default". -- Return { "ok": true, "updatedAt": "..." }. -``` - -## Optional `GET /api/health` - -Returns: - -```json -{ - "ok": true -} -``` - -Useful for Docker health checks. - ---- - -# Scene handling - -Create `lib/scene.ts`. - -Responsibilities: - -```text -- define EmptyScene -- normalize incoming scene -- remove unwanted volatile appState fields -- cap payload size -``` - -Recommended payload size cap for v1: - -```text -25 MB -``` - -If the scene exceeds this, return HTTP `413 Payload Too Large`. - -Reason: Excalidraw embedded images are stored under `files`; they can make scene JSON large. - ---- - -# Frontend behavior - -## Page load - -```text -1. Render loading state. -2. Fetch /api/drawing. -3. Pass returned scene to Excalidraw as initialData. -4. Render editor full-screen. -``` - -The official integration docs show that the Excalidraw component should be used client-side in Next.js, with `"use client"` and the Excalidraw CSS import. ([Excalidraw Docs][5]) - -## Autosave - -Use Excalidraw’s `onChange`. - -Pseudo-flow: - -```tsx -onChange={(elements, appState, files) => { - setSaveStatus("dirty"); - debouncedSave({ elements, appState, files }); -}} -``` - -Debounced save: - -```text -- wait 1500 ms after last change -- POST to /api/drawing -- set status Saving… while request in flight -- set status Saved after success -- set status Save failed after failure -``` - -Important: Do not save on every pointer movement without debounce. - -## Save status UI - -A small fixed indicator: - -```text -top-right: - Saved - Saving… - Save failed -``` - -Optionally show last saved timestamp. - ---- - -# Export behavior - -## `.excalidraw` export - -Implement client-side export by taking the current scene and downloading JSON. - -Filename: - -```text -drawing-YYYY-MM-DD-HHMM.excalidraw -``` - -Content: - -```json -{ - "type": "excalidraw", - "version": 2, - "source": "https://excalidraw.com", - "elements": [], - "appState": {}, - "files": {} -} -``` - -This keeps compatibility with regular Excalidraw import/export. The Excalidraw repo describes the open `.excalidraw` JSON file format as a supported feature. ([GitHub][6]) - -## PNG export - -Use Excalidraw export utilities client-side. - -The docs show `exportToCanvas` exported from `@excalidraw/excalidraw`; it takes `elements`, `appState`, and export dimensions. ([Excalidraw Docs][3]) - -Implementation idea: - -```tsx -import { exportToCanvas } from "@excalidraw/excalidraw"; - -const canvas = await exportToCanvas({ - elements, - appState: { - ...appState, - exportBackground: true, - }, - files, - getDimensions: (width, height) => ({ - width, - height, - scale: 2, - }), -}); - -const blob = await new Promise((resolve) => - canvas.toBlob((blob) => resolve(blob!), "image/png"), -); -``` - -Then trigger browser download. - -Do PNG export client-side first. It avoids needing headless browser rendering on the server. - ---- - -# Important implementation detail: access to current scene - -Use `excalidrawAPI`. - -The Excalidraw API exposes methods such as `getFiles`. ([Excalidraw Docs][7]) - -Store latest values from `onChange` in React refs: - -```tsx -const latestSceneRef = useRef({ - elements: [], - appState: {}, - files: {}, -}); -``` - -Update it inside `onChange`. - -Use that ref for export buttons. - ---- - -# Suggested files - -## `package.json` - -```json -{ - "name": "excalidraw-autosave", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@excalidraw/excalidraw": "latest", - "better-sqlite3": "latest", - "next": "latest", - "react": "latest", - "react-dom": "latest" - }, - "devDependencies": { - "@types/better-sqlite3": "latest", - "@types/node": "latest", - "@types/react": "latest", - "@types/react-dom": "latest", - "typescript": "latest" - } -} -``` - -## `next.config.ts` - -```ts -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - output: "standalone", -}; - -export default nextConfig; -``` - ---- - -# Database module - -## `lib/db.ts` - -Requirements: - -```text -- Read DATABASE_PATH from env -- Default to /data/excalidraw.sqlite -- Ensure parent directory exists -- Initialize schema -- Export getDrawing() and saveDrawing() -``` - -Pseudo-code: - -```ts -import Database from "better-sqlite3"; -import fs from "node:fs"; -import path from "node:path"; - -const dbPath = process.env.DATABASE_PATH ?? "/data/excalidraw.sqlite"; -fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - -const db = new Database(dbPath); - -db.pragma("journal_mode = WAL"); - -db.exec(` - CREATE TABLE IF NOT EXISTS drawings ( - id TEXT PRIMARY KEY, - data TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP - ) -`); - -export function getDrawing(id = "default") { - const row = db.prepare("SELECT data FROM drawings WHERE id = ?").get(id) as - | { data: string } - | undefined; - - return row ? JSON.parse(row.data) : null; -} - -export function saveDrawing(data: unknown, id = "default") { - const serialized = JSON.stringify(data); - - db.prepare( - ` - INSERT INTO drawings (id, data, created_at, updated_at) - VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT(id) DO UPDATE SET - data = excluded.data, - updated_at = CURRENT_TIMESTAMP - `, - ).run(id, serialized); - - return new Date().toISOString(); -} -``` - ---- - -# API route - -## `app/api/drawing/route.ts` - -Requirements: - -```text -- Force Node runtime, not Edge -- GET returns saved or empty scene -- POST saves normalized scene -- Handle invalid JSON -- Enforce payload size cap -``` - -Add: - -```ts -export const runtime = "nodejs"; -``` - -Pseudo-code: - -```ts -import { NextResponse } from "next/server"; -import { getDrawing, saveDrawing } from "@/lib/db"; -import { emptyScene, normalizeScene } from "@/lib/scene"; - -export const runtime = "nodejs"; - -export async function GET() { - const saved = getDrawing("default"); - return NextResponse.json(saved ?? emptyScene()); -} - -export async function POST(req: Request) { - const text = await req.text(); - - if (text.length > 25 * 1024 * 1024) { - return NextResponse.json( - { ok: false, error: "Scene too large" }, - { status: 413 }, - ); - } - - let raw: unknown; - - try { - raw = JSON.parse(text); - } catch { - return NextResponse.json( - { ok: false, error: "Invalid JSON" }, - { status: 400 }, - ); - } - - const scene = normalizeScene(raw); - const updatedAt = saveDrawing(scene, "default"); - - return NextResponse.json({ ok: true, updatedAt }); -} -``` - ---- - -# Client component - -## `components/ExcalidrawClient.tsx` - -Requirements: - -```text -- "use client" -- import Excalidraw and CSS -- fetch initial data -- render full viewport -- onChange debounced autosave -- export JSON button -- export PNG button -- save status -``` - -Important: - -```tsx -import "@excalidraw/excalidraw/index.css"; -``` - -Use a custom debounce instead of lodash to avoid another dependency. - ---- - -# CSS/UI - -Keep UI minimal. - -```text -- Editor fills viewport. -- Floating toolbar at top-right. -- Buttons: - - Export PNG - - Export .excalidraw -- Save status badge: - - Saved - - Saving… - - Failed -``` - -Do not attempt to reproduce Excalidraw’s own toolbar. - ---- - -# Docker - -## `Dockerfile` - -Use Next.js standalone output. - -```dockerfile -FROM node:22-alpine AS deps -WORKDIR /app -COPY package.json package-lock.json* ./ -RUN npm install - -FROM node:22-alpine AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -RUN npm run build - -FROM node:22-alpine AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 -ENV DATABASE_PATH=/data/excalidraw.sqlite - -RUN mkdir -p /data - -COPY --from=builder /app/public ./public -COPY --from=builder /app/.next/standalone ./ -COPY --from=builder /app/.next/static ./.next/static - -EXPOSE 3000 - -CMD ["node", "server.js"] -``` - -## `docker-compose.yml` - -```yaml -services: - excalidraw-autosave: - build: . - container_name: excalidraw-autosave - expose: - - "3000" - environment: - DATABASE_PATH: /data/excalidraw.sqlite - volumes: - - ./data:/data - restart: unless-stopped - - caddy: - image: caddy:latest - container_name: excalidraw-caddy - depends_on: - - excalidraw-autosave - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile.example:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - restart: unless-stopped - -volumes: - caddy_data: - caddy_config: -``` - -## `Caddyfile.example` - -```caddyfile -draw.example.com { - basic_auth { - zero REPLACE_WITH_CADDY_HASH - } - - reverse_proxy excalidraw-autosave:3000 -} -``` - -Generate password hash: - -```bash -docker run --rm -it caddy:latest caddy hash-password -``` - ---- - -# Backup plan - -SQLite file: - -```text -./data/excalidraw.sqlite -``` - -Backup command: - -```bash -mkdir -p ./backups -sqlite3 ./data/excalidraw.sqlite ".backup './backups/excalidraw-$(date +%F-%H%M).sqlite'" -``` - -Recommended cron: - -```cron -0 3 * * * cd /path/to/excalidraw-autosave && sqlite3 ./data/excalidraw.sqlite ".backup './backups/excalidraw-$(date +\%F-\%H\%M).sqlite'" -``` - -Also back up `.excalidraw` exports manually for critical drawings. - ---- - -# Security model - -V1 app assumes: - -```text -- It is not public without reverse proxy auth. -- Caddy handles TLS. -- Caddy handles basic_auth. -- The app does not implement users. -- The app should not expose port 3000 to the public internet. -``` - -In Compose, use: - -```yaml -expose: - - "3000" -``` - -Do **not** use: - -```yaml -ports: - - "3000:3000" -``` - -unless testing locally. - ---- - -# Acceptance criteria - -Codex should finish with a repo that satisfies: - -```text -1. `npm run dev` opens editor locally. -2. Drawing reloads after refresh. -3. Drawing persists after app restart. -4. Docker Compose starts Caddy + app. -5. SQLite DB is created under ./data. -6. No direct public app port is exposed. -7. Export PNG downloads a PNG. -8. Export .excalidraw downloads editable JSON. -9. Autosave status visibly changes. -10. No collaboration code exists. -``` - -Manual test: - -```text -1. Start app. -2. Draw rectangle and text. -3. Wait until status says Saved. -4. Refresh browser. -5. Confirm rectangle and text remain. -6. Restart container. -7. Confirm drawing remains. -8. Export PNG. -9. Export .excalidraw. -10. Import .excalidraw into official Excalidraw and confirm it opens. -``` - ---- - -# Future V2, only after V1 works - -Add these later, not now: - -```text -- multiple named drawings -- drawing list page -- thumbnails -- duplicate drawing -- delete drawing -- server-side export endpoint -- periodic backup UI -- WebDAV/S3 export -- basic file manager -``` - -For V2 multiple drawings, change routes to: - -```text -GET /api/drawings -POST /api/drawings -GET /api/drawings/:id -PUT /api/drawings/:id -DELETE /api/drawings/:id -``` - -Schema: - -```sql -CREATE TABLE drawings ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - data TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP -); -``` - ---- - -# Instruction - -Build the V1 exactly as specified. Keep code minimal. Do not add accounts, collaboration, sharing, Firebase, Prisma, Tailwind, or extra abstractions. Use the official `@excalidraw/excalidraw` package and SQLite persistence through `better-sqlite3`. Use Caddy for auth in the deployment example.