Initial boilerplate scaffold for continuum-app
This commit is contained in:
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user