v1.0.0 — Hexagonal architecture rewrite
All checks were successful
CI / ci (push) Successful in 7m16s

Restructure the monolithic 252-line main.rs into a 10-crate workspace
with clean hexagonal architecture, swappable adapters, and a production-
ready deployment pipeline.

Backend architecture:
- domain: Canvas, Color/Position/PixelUpdate value objects, port traits
  (CanvasStore, CanvasPersistence, EventBroadcaster), BroadcastEvent
- application: use cases (place_pixel, get_state, save/restore snapshot,
  connect/disconnect), AppState with Arc snapshot cache
- config: AppConfig structs + ConfigSource trait
- api-types: shared DTOs, event name constants
- adapters: config-env, canvas-file, http-axum (rust-embed), socketio,
  websocket — all behind port traits, swappable via feature flags
- server: composition root with graceful shutdown (SIGTERM/SIGINT)

Frontend:
- Transport abstraction: Socket.IO and native WebSocket via VITE_TRANSPORT
- Canvas zoom/pan with mouse wheel, pinch-to-zoom, and +/- buttons
- ImageData rendering (~50x faster than fillRect loop)
- Touch support, responsive CSS scaling, mobile-friendly layout
- OG/Twitter Card meta tags for rich link previews

Production:
- Docker: musl static build on scratch — 2.73MB image
- CI workflows for Gitea and GitHub Actions (fmt, clippy, test, Docker push)
- deploy.sh for private registry
- 39 unit tests across domain and application
- Zero unwraps, zero unsafe, graceful error handling with tracing
- Periodic canvas snapshots with rotation, restored on startup
- All config via environment variables with typed defaults
This commit is contained in:
2026-08-18 01:41:40 +02:00
parent e9bea5e1e5
commit f652785acb
85 changed files with 4240 additions and 3736 deletions

View File

@@ -1,58 +1,88 @@
import { connectToWS } from "./socket.js";
import "./canvas.js";
import "./counter.js";
import { updateCountdown } from "./counter.js";
import { checkEndpoint, pixelSize } from "./constants.js";
import { handleSocketEvents } from "./canvas.js";
import "./challenge.js"
import { createSocketConnection } from "./infrastructure/socket-client.js";
import { checkServer } from "./infrastructure/api.js";
import { createCanvasRenderer } from "./ui/canvas-renderer.js";
import { createColorPalette } from "./ui/color-palette.js";
import { createPixelPlacer } from "./ui/pixel-placer.js";
import { startCooldownDisplay } from "./ui/cooldown-display.js";
import { createCanvasViewport } from "./ui/canvas-viewport.js";
import { setPixel } from "./domain/canvas-state.js";
import { getCanvasCoords } from "./domain/coords.js";
const isDebug = import.meta.env.VITE_IS_DEBUG === "true";
const currentSoldiersSpan = document.getElementById("current-soldiers");
let coords = [];
const canvas = document.getElementById("canvas");
const canvasEl = document.getElementById("canvas");
const coordsText = document.getElementById("coords");
const ogCanvasStyle = canvas.style.display;
canvas.style.display = "none";
const currentSoldiersSpan = document.getElementById("current-soldiers");
const statusEl = document.getElementById("connection-status");
fetch(checkEndpoint)
const savedDisplay = canvasEl.style.display;
canvasEl.style.display = "none";
let canvasState = [];
const renderer = createCanvasRenderer(canvasEl);
const palette = createColorPalette();
startCooldownDisplay();
createCanvasViewport(canvasEl);
canvasEl.addEventListener("mousemove", (event) => {
const { x, y } = getCanvasCoords(event, canvasEl);
coordsText.textContent = `${x}, ${y}`;
});
document.getElementById("save-canvas").addEventListener("click", () => {
const a = document.createElement("a");
a.href = renderer.toDataURL();
a.download = "canvas.png";
a.click();
});
const showStatus = (message, isError) => {
if (!statusEl) return;
statusEl.textContent = message;
statusEl.className = isError
? "text-red-500 text-sm"
: "text-green-500 text-sm";
};
checkServer()
.then((response) => {
if (response.ok) {
const socket = connectToWS();
if (!response.ok) throw new Error("Server unavailable");
socket.on("connect", () => {
canvas.style.display = ogCanvasStyle;
console.log("connect");
});
const socket = createSocketConnection();
socket.on("error", (message) => {
alert(message);
});
socket.on("connect", () => {
canvasEl.style.display = savedDisplay;
showStatus("Connected", false);
});
socket.on("current_soldiers", (currentSoldiers) => {
currentSoldiersSpan.textContent = currentSoldiers;
});
socket.on("canvas_state", (data) => {
canvasState = data;
renderer.drawState(data);
});
handleSocketEvents(socket);
socket.on("error", (message) => showStatus(message, true));
requestAnimationFrame(updateCountdown);
socket.on("current_soldiers", (count) => {
currentSoldiersSpan.textContent = count;
});
window.addEventListener("mousemove", (event) => {
// get coordinates of the mouse inside the canvas
const rect = canvas.getBoundingClientRect();
const x = Math.floor((event.clientX - rect.left) / pixelSize);
const y = Math.floor((event.clientY - rect.top) / pixelSize);
coords = [x, y];
socket.on("pixel-updated", (update) => {
renderer.drawPixel(update.x, update.y, update.color);
setPixel(canvasState, update.x, update.y, update.color);
});
coordsText.textContent = `${x}, ${y}`;
});
} else {
throw new Error("Can't connect to the server");
}
socket.on("disconnect", () => {
showStatus("Disconnected — reconnecting...", true);
});
createPixelPlacer({
canvas: canvasEl,
renderer,
getColor: palette.getColor,
getState: () => canvasState,
socket,
});
})
.catch((error) => {
alert(
"You have already connected to the server from another tab or window. Please close the other tab or window and refresh this page."
);
.catch(() => {
showStatus("Cannot connect to server", true);
});