feat: Add canvas snapshots to save and restore full canvas state

Implements save/restore system that captures HTML base, injected CSS,
executed scripts, and floating Vue windows with their full definitions.
Adds 4 MCP tools, backend CRUD API, Pinia store, and script logger.
This commit is contained in:
2026-02-14 23:08:33 -06:00
parent 5fd57ba70f
commit 3f15aa590b
13 changed files with 641 additions and 119 deletions

View File

@@ -85,6 +85,17 @@ export function runMigrations(db: Database) {
)
`)
// Canvas snapshots table
db.run(`
CREATE TABLE IF NOT EXISTS canvas_snapshots (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
data TEXT NOT NULL,
thumbnail TEXT,
created_at INTEGER NOT NULL
)
`)
// Voice recordings table (for training custom speech models)
db.run(`
CREATE TABLE IF NOT EXISTS voice_recordings (

View File

@@ -10,6 +10,7 @@ import { handleTables, handleStats, handleTableSchema, handleTableData, handleQu
import { handleWhisperRoutes } from './whisper'
import { handleRecordingsRoutes } from './recordings'
import { handleClaudeStatus } from './claude-status'
import { handleSnapshots, handleSnapshotById } from './snapshots'
import { handleGitStatus, handleGitDiff, handleGitLog, handleGitLogCommit, handleGitCompare, handleGitBranches, handleGitCurrentBranch, handleGitTree, handleGitFile } from './git'
export async function handleRequest(req: Request): Promise<Response> {
@@ -145,6 +146,18 @@ export async function handleRequest(req: Request): Promise<Response> {
if (res) return res
}
// Snapshots
if (path === '/api/snapshots') {
const res = await handleSnapshots(req)
if (res) return res
}
if (path.startsWith('/api/snapshots/')) {
const id = path.split('/').pop()!
const res = await handleSnapshotById(req, id)
if (res) return res
}
// Gitea
if (path === '/api/gitea/repo' && req.method === 'POST') {
return handleGiteaRepo(req)

View File

@@ -0,0 +1,49 @@
import { db } from '../db'
import { jsonResponse, errorResponse } from '../utils/cors'
export async function handleSnapshots(req: Request) {
if (req.method === 'GET') {
const rows = db.query(
'SELECT id, name, thumbnail, created_at FROM canvas_snapshots ORDER BY created_at DESC'
).all()
return jsonResponse(rows)
}
if (req.method === 'POST') {
const body = await req.json()
const id = body.id || `snap-${Date.now()}`
const stmt = db.prepare(
'INSERT OR REPLACE INTO canvas_snapshots (id, name, data, thumbnail, created_at) VALUES (?, ?, ?, ?, ?)'
)
stmt.run(
id,
body.name,
typeof body.data === 'string' ? body.data : JSON.stringify(body.data),
body.thumbnail || null,
body.created_at || Date.now()
)
return jsonResponse({ success: true, id })
}
return null
}
export async function handleSnapshotById(req: Request, id: string) {
if (req.method === 'GET') {
const row = db.query('SELECT * FROM canvas_snapshots WHERE id = ?').get(id) as any
if (!row) {
return errorResponse('Snapshot not found', 404)
}
return jsonResponse({
...row,
data: JSON.parse(row.data)
})
}
if (req.method === 'DELETE') {
db.run('DELETE FROM canvas_snapshots WHERE id = ?', [id])
return jsonResponse({ success: true })
}
return null
}