Initial boilerplate scaffold for continuum-app

This commit is contained in:
2026-08-28 16:21:23 +00:00
commit 4cb2e82a92
25 changed files with 810 additions and 0 deletions
+19
View File
@@ -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"
}
}
+24
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -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"
]
}
+39
View File
@@ -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");
}
+48
View File
@@ -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/**"]
}
}
}
+73
View File
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useTelemetryStore } from "~/stores/telemetry";
const telemetry = useTelemetryStore();
const user = useSupabaseUser();
onMounted(() => {
telemetry.connect();
});
onBeforeUnmount(() => {
telemetry.disconnect();
});
</script>
<template>
<div class="app-shell">
<header class="app-shell__header">
<span class="app-shell__brand">Continuum</span>
<span class="app-shell__status" :class="telemetry.connected ? 'is-online' : 'is-offline'">
{{ telemetry.connected ? "Live" : "Reconnecting…" }}
</span>
<span v-if="user" class="app-shell__user">{{ user.email }}</span>
</header>
<main class="app-shell__main">
<NuxtPage />
</main>
</div>
</template>
<style>
:root {
color-scheme: dark;
font-family: system-ui, -apple-system, sans-serif;
}
body {
margin: 0;
background: #0b0d12;
color: #e6e8ec;
}
.app-shell__header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.25rem;
border-bottom: 1px solid #1f232c;
}
.app-shell__brand {
font-weight: 700;
letter-spacing: 0.02em;
}
.app-shell__status.is-online {
color: #35d07f;
}
.app-shell__status.is-offline {
color: #f2a33c;
}
.app-shell__user {
margin-left: auto;
opacity: 0.7;
font-size: 0.875rem;
}
.app-shell__main {
padding: 1.25rem;
}
</style>
+47
View File
@@ -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,
},
});
+26
View File
@@ -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"
}
}
+35
View File
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { PrinterCard } from "@continuum/ui";
import { useTelemetryStore } from "~/stores/telemetry";
const telemetry = useTelemetryStore();
const config = useRuntimeConfig();
const { data: printers } = await useFetch(`${config.public.apiBase}/printers`, {
headers: useRequestHeaders(["cookie"]),
});
</script>
<template>
<div class="fleet">
<h1>Fleet overview</h1>
<div class="fleet__grid">
<PrinterCard
v-for="printer in printers ?? []"
:key="printer.id"
:printer="printer"
:live="telemetry.printers[printer.id]"
/>
</div>
<p v-if="!printers?.length">No printers registered yet.</p>
</div>
</template>
<style scoped>
.fleet__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
const email = ref("");
const sent = ref(false);
const error = ref<string | null>(null);
const client = useSupabaseClient();
async function sendMagicLink() {
error.value = null;
const { error: authError } = await client.auth.signInWithOtp({ email: email.value });
if (authError) {
error.value = authError.message;
return;
}
sent.value = true;
}
</script>
<template>
<div class="login">
<h1>Sign in to Continuum</h1>
<form v-if="!sent" @submit.prevent="sendMagicLink">
<input v-model="email" type="email" placeholder="you@farm.com" required />
<button type="submit">Send magic link</button>
</form>
<p v-else>Check your inbox for a sign-in link.</p>
<p v-if="error" class="login__error">{{ error }}</p>
</div>
</template>
<style scoped>
.login {
max-width: 320px;
margin: 4rem auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.login__error {
color: #f2604c;
}
</style>
+86
View File
@@ -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<string, PrinterTelemetry>;
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;
},
},
});
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}