Phase 3: Express backend, SQLite persistence, auto-save

- server/db.ts: sql.js with migration system (hex_maps, hexes, hex_features)
- server/routes/maps.ts: CRUD for hex maps
- server/routes/hexes.ts: Bulk hex upsert, region load, sparse storage
- server/index.ts: Express 5, CORS, tile serving, SPA fallback
- src/data/api-client.ts: Frontend HTTP client for all API endpoints
- src/main.ts: Auto-save with 1s debounce, load map state on startup
- Port 3002 (Kiepenkerl uses 3001)
- Graceful fallback when API unavailable (works without server too)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-04-07 10:45:37 +00:00
parent 0e2903b789
commit 367ba8af07
8 changed files with 523 additions and 6 deletions

View File

@@ -15,6 +15,7 @@ import {
enforceEdgeConstraints,
applyConstraintActions,
} from '../core/edge-connectivity.js';
import * as api from './data/api-client.js';
// --- State ---
const hexMap = new HexMap();
@@ -23,6 +24,8 @@ let selectedTerrain: TerrainType | null = null;
let selectedHex: AxialCoord | null = null;
let hexSize = 48;
const origin = { x: 0, y: 0 };
let currentMapId: number | null = null;
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
// --- Init Map ---
const map = initMap('map');
@@ -41,7 +44,7 @@ hexLayer.addTo(map);
const sidebarEl = document.getElementById('sidebar')!;
const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl);
const toolbarUI = createToolbar(toolbar, (mode) => {
createToolbar(toolbar, (mode) => {
currentMode = mode;
terrainPickerUI.setMode(mode);
});
@@ -76,6 +79,28 @@ function rebuildHexLayer(showGrid: boolean, opacity: number) {
reattachInteraction();
}
// --- Auto-save (debounced) ---
function scheduleSave() {
if (!currentMapId) return;
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
if (!currentMapId || !hexMap.dirty) return;
try {
const data = hexMap.serialize();
await api.saveHexes(currentMapId, data.hexes.map(h => ({
q: h.q,
r: h.r,
base: h.base,
features: h.features.map(f => ({ terrainId: f.terrainId, edgeMask: f.edgeMask })),
})));
hexMap.markClean();
console.log(`[save] Saved ${data.hexes.length} hexes`);
} catch (err) {
console.error('[save] Failed:', err);
}
}, 1000);
}
// --- Interaction handlers ---
function handleSelect(event: HexClickEvent) {
@@ -90,6 +115,7 @@ function handlePaint(event: HexClickEvent) {
hexLayer.redraw();
selectedHex = event.coord;
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
scheduleSave();
}
function handleFeature(event: HexClickEvent) {
@@ -112,6 +138,7 @@ function handleFeature(event: HexClickEvent) {
hexLayer.setSelectedHex(selectedHex);
hexLayer.redraw();
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
scheduleSave();
}
// --- Hex Interaction ---
@@ -121,13 +148,11 @@ function reattachInteraction() {
detachInteraction?.();
detachInteraction = attachHexInteraction(
map, hexSize, origin,
// Click handler
(event) => {
if (currentMode === 'select') handleSelect(event);
else if (currentMode === 'paint') handlePaint(event);
else if (currentMode === 'feature') handleFeature(event);
},
// Drag-paint handler (only active in paint mode)
(event) => {
if (currentMode === 'paint') handlePaint(event);
},
@@ -135,3 +160,36 @@ function reattachInteraction() {
}
reattachInteraction();
// --- Load or create map from API ---
async function init() {
try {
const maps = await api.listMaps();
if (maps.length > 0) {
currentMapId = maps[0].id;
console.log(`[init] Loading map "${maps[0].name}" (id: ${currentMapId})`);
} else {
const result = await api.createMap({ name: 'Default Map' });
currentMapId = result.id;
console.log(`[init] Created new map (id: ${currentMapId})`);
}
// Load existing hexes
const hexes = await api.loadHexes(currentMapId);
if (hexes.length > 0) {
for (const hex of hexes) {
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
base: hex.base,
features: hex.features,
});
}
hexMap.markClean();
hexLayer.redraw();
console.log(`[init] Loaded ${hexes.length} hexes`);
}
} catch (err) {
console.warn('[init] API not available, running without persistence:', err);
}
}
init();