commit 4cb2e82a92dbeb862f4648989e871456f3ba2256 Author: Iwo Strzeboński Date: Fri Aug 28 16:21:23 2026 +0000 Initial boilerplate scaffold for continuum-app diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e2ff5e2 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# --- apps/web (Nuxt) runtime config --- +# Public (exposed to the client bundle) +NUXT_PUBLIC_API_BASE=https://api.continuum.local +NUXT_PUBLIC_WS_BASE=wss://api.continuum.local +NUXT_PUBLIC_SUPABASE_URL=https://xxxxxxxx.supabase.co +NUXT_PUBLIC_SUPABASE_ANON_KEY=replace-with-anon-key +NUXT_PUBLIC_GO2RTC_STREAM_BASE=https://edge.continuum.local/streams + +# Desktop (Tauri) build-time +TAURI_SIGNING_PRIVATE_KEY= +TAURI_SIGNING_PRIVATE_KEY_PASSWORD= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bd49bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +node_modules/ +.nuxt/ +.output/ +dist/ +.env +.env.local +*.log +.DS_Store + +# Tauri +apps/desktop/src-tauri/target/ +apps/desktop/src-tauri/gen/ + +# Rust +**/*.rs.bk + +# pnpm +.pnpm-store/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b0799d --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# continuum-app + +Operator console for the Continuum print farm platform. A single Nuxt 3 SPA +(`apps/web`) shared between the browser and a Tauri 2 desktop shell +(`apps/desktop`), plus a shared component library (`packages/ui`). + +## Layout + +``` +apps/ + web/ Nuxt 3 SPA (ssr: false) — Supabase auth, Pinia telemetry store + desktop/ Tauri 2 shell wrapping apps/web's static build +packages/ + ui/ Shared Vue components: PrinterCard, TemperatureGraph, WebRTCStreamView +``` + +## Getting started + +```bash +pnpm install +cp .env.example apps/web/.env +pnpm dev:web # browser dev server on :3000 +pnpm dev:desktop # Tauri dev shell (requires Rust toolchain) +``` + +`apps/web` builds as a static SPA so the exact same `apps/web/.output/public` +bundle is served in the browser and embedded into the Tauri desktop app. diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..4de357b --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,19 @@ +{ + "name": "@continuum/desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "tauri": "tauri", + "dev": "tauri dev", + "build": "tauri build" + }, + "dependencies": { + "@tauri-apps/api": "^2.1.1", + "@tauri-apps/plugin-fs": "^2.0.3", + "@tauri-apps/plugin-shell": "^2.0.2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.1.0" + } +} diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..d95727e --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "continuum-desktop" +version = "0.1.0" +description = "Continuum operator console desktop shell" +edition = "2021" +rust-version = "1.77" + +[build-dependencies] +tauri-build = { version = "2.0", features = [] } + +[dependencies] +tauri = { version = "2.1", features = ["tray-icon"] } +tauri-plugin-fs = "2.0" +tauri-plugin-shell = "2.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[lib] +name = "continuum_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "continuum-desktop" +path = "src/main.rs" diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..a5c006b --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capabilities granted to the main Continuum window", + "windows": ["main"], + "permissions": [ + "core:default", + "fs:default", + "fs:allow-read-file", + "fs:allow-write-file", + "fs:scope-appdata", + "shell:allow-open" + ] +} diff --git a/apps/desktop/src-tauri/icons/.gitkeep b/apps/desktop/src-tauri/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..eddc209 --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,39 @@ +// Prevents an additional console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{ + menu::{Menu, MenuItem}, + tray::TrayIconBuilder, + Manager, +}; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_shell::init()) + .setup(|app| { + let show = MenuItem::with_id(app, "show", "Show Continuum", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + + TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu) + .tooltip("Continuum — print farm console") + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + "quit" => app.exit(0), + _ => {} + }) + .build(app)?; + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running the Continuum desktop shell"); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..22a4e0a --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Continuum", + "version": "0.1.0", + "identifier": "com.continuum3d.desktop", + "build": { + "beforeDevCommand": "pnpm --filter @continuum/web dev", + "beforeBuildCommand": "pnpm --filter @continuum/web generate", + "devUrl": "http://localhost:3000", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Continuum", + "width": 1280, + "height": 800, + "minWidth": 960, + "minHeight": 600, + "resizable": true + } + ], + "security": { + "csp": null + }, + "trayIcon": { + "iconPath": "icons/icon.png", + "iconAsTemplate": true + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "plugins": { + "fs": { + "scope": ["$APPDATA/continuum/**", "$DOWNLOAD/**"] + } + } +} diff --git a/apps/web/app.vue b/apps/web/app.vue new file mode 100644 index 0000000..76577a7 --- /dev/null +++ b/apps/web/app.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/apps/web/nuxt.config.ts b/apps/web/nuxt.config.ts new file mode 100644 index 0000000..51dbf33 --- /dev/null +++ b/apps/web/nuxt.config.ts @@ -0,0 +1,47 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: "2026-01-01", + + // Disabled so the exact same static bundle can be served from the browser + // and embedded verbatim into the Tauri desktop shell. + ssr: false, + + devtools: { enabled: true }, + + modules: ["@nuxtjs/supabase", "@pinia/nuxt"], + + supabase: { + redirect: true, + redirectOptions: { + login: "/login", + callback: "/confirm", + exclude: ["/login"], + }, + }, + + runtimeConfig: { + public: { + apiBase: process.env.NUXT_PUBLIC_API_BASE || "http://localhost:3001", + wsBase: process.env.NUXT_PUBLIC_WS_BASE || "ws://localhost:3001", + go2rtcStreamBase: process.env.NUXT_PUBLIC_GO2RTC_STREAM_BASE || "http://localhost:1984", + }, + }, + + // apps/desktop's src-tauri config points at this static output directory. + nitro: { + output: { + publicDir: "../desktop/dist", + }, + }, + + vite: { + // Tauri needs a fixed, predictable dev server port. + server: { + strictPort: true, + }, + }, + + typescript: { + strict: true, + }, +}); diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..829168d --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "@continuum/web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "generate": "nuxt generate", + "preview": "nuxt preview", + "typecheck": "nuxt typecheck", + "lint": "eslint ." + }, + "dependencies": { + "@continuum/ui": "workspace:*", + "@nuxtjs/supabase": "^1.4.0", + "@pinia/nuxt": "^0.5.5", + "nuxt": "^3.13.0", + "pinia": "^2.2.2", + "vue": "^3.5.0", + "vue-router": "^4.4.3" + }, + "devDependencies": { + "typescript": "^5.6.2" + } +} diff --git a/apps/web/pages/index.vue b/apps/web/pages/index.vue new file mode 100644 index 0000000..a41b74f --- /dev/null +++ b/apps/web/pages/index.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/apps/web/pages/login.vue b/apps/web/pages/login.vue new file mode 100644 index 0000000..c91a76f --- /dev/null +++ b/apps/web/pages/login.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/apps/web/stores/telemetry.ts b/apps/web/stores/telemetry.ts new file mode 100644 index 0000000..b8c8d6e --- /dev/null +++ b/apps/web/stores/telemetry.ts @@ -0,0 +1,86 @@ +import { defineStore } from "pinia"; + +export interface PrinterTelemetry { + printerId: string; + farmId: string; + state: "idle" | "printing" | "paused" | "error" | "offline"; + nozzleTempC: number; + bedTempC: number; + progressPct: number; + updatedAt: string; +} + +interface TelemetryState { + socket: WebSocket | null; + connected: boolean; + printers: Record; + reconnectAttempt: number; +} + +export const useTelemetryStore = defineStore("telemetry", { + state: (): TelemetryState => ({ + socket: null, + connected: false, + printers: {}, + reconnectAttempt: 0, + }), + + getters: { + printerList: (state) => Object.values(state.printers), + }, + + actions: { + connect() { + if (this.socket) return; + + const config = useRuntimeConfig(); + const client = useSupabaseClient(); + + client.auth.getSession().then(({ data }) => { + const token = data.session?.access_token ?? ""; + const url = `${config.public.wsBase}/ws/telemetry?token=${encodeURIComponent(token)}`; + this.open(url); + }); + }, + + open(url: string) { + const socket = new WebSocket(url); + + socket.onopen = () => { + this.connected = true; + this.reconnectAttempt = 0; + }; + + socket.onmessage = (event) => { + try { + const payload = JSON.parse(event.data) as { type: string; data: PrinterTelemetry }; + if (payload.type === "printer.telemetry") { + this.printers[payload.data.printerId] = payload.data; + } + } catch { + // Ignore malformed frames rather than crashing the UI. + } + }; + + socket.onclose = () => { + this.connected = false; + this.socket = null; + const delay = Math.min(30_000, 1_000 * 2 ** this.reconnectAttempt); + this.reconnectAttempt += 1; + setTimeout(() => this.connect(), delay); + }; + + socket.onerror = () => { + socket.close(); + }; + + this.socket = socket; + }, + + disconnect() { + this.socket?.close(); + this.socket = null; + this.connected = false; + }, + }, +}); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..4b34df1 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad5d490 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "continuum-app", + "private": true, + "version": "0.1.0", + "description": "Continuum print farm operator console — Nuxt SPA + Tauri 2 desktop shell", + "packageManager": "pnpm@9.9.0", + "scripts": { + "dev:web": "pnpm --filter @continuum/web dev", + "dev:desktop": "pnpm --filter @continuum/desktop tauri dev", + "build:web": "pnpm --filter @continuum/web build", + "build:desktop": "pnpm --filter @continuum/desktop tauri build", + "lint": "pnpm -r lint", + "typecheck": "pnpm -r typecheck" + }, + "devDependencies": { + "typescript": "^5.6.2" + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..66d969f --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,19 @@ +{ + "name": "@continuum/ui", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "vue-tsc --noEmit", + "lint": "eslint ." + }, + "peerDependencies": { + "vue": "^3.5.0" + }, + "devDependencies": { + "typescript": "^5.6.2", + "vue-tsc": "^2.1.6" + } +} diff --git a/packages/ui/src/components/PrinterCard.vue b/packages/ui/src/components/PrinterCard.vue new file mode 100644 index 0000000..bb5b5f5 --- /dev/null +++ b/packages/ui/src/components/PrinterCard.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/ui/src/components/TemperatureGraph.vue b/packages/ui/src/components/TemperatureGraph.vue new file mode 100644 index 0000000..d5f5cee --- /dev/null +++ b/packages/ui/src/components/TemperatureGraph.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/packages/ui/src/components/WebRTCStreamView.vue b/packages/ui/src/components/WebRTCStreamView.vue new file mode 100644 index 0000000..1498295 --- /dev/null +++ b/packages/ui/src/components/WebRTCStreamView.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..96140a5 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,3 @@ +export { default as PrinterCard } from "./components/PrinterCard.vue"; +export { default as TemperatureGraph } from "./components/TemperatureGraph.vue"; +export { default as WebRTCStreamView } from "./components/WebRTCStreamView.vue"; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..67f37f9 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "declaration": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*"