mirror of
https://github.com/Vale54321/schafkop-neu.git
synced 2025-12-15 11:29:32 +01:00
feat: integrate Tailwind CSS for improved styling and layout
- Added Tailwind CSS and its Vite plugin to the frontend project. - Updated App.svelte to enhance UI with Tailwind classes, including a new layout for the serial monitor and command tester. - Improved logging functionality by increasing log history and adding more detailed log messages. - Refactored SendBox.svelte for consistent styling with Tailwind. - Enhanced Pico firmware to support structured event logging and command parsing. - Updated README_PICO_SERIAL.md to provide comprehensive documentation on serial communication and backend integration. - Added .dockerignore to optimize Docker builds by excluding unnecessary files.
This commit is contained in:
@@ -1,20 +1,48 @@
|
||||
# Multi-stage build for SvelteKit (static client + node server fallback)
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
######## Combined Frontend + Backend build (single Node runtime) ########
|
||||
# This Dockerfile now builds BOTH the Svelte frontend and the NestJS backend
|
||||
# and serves the compiled frontend through the backend (Express static).
|
||||
|
||||
# Install deps (use package-lock if present)
|
||||
COPY package*.json ./
|
||||
########################
|
||||
# 1) Frontend build #
|
||||
########################
|
||||
FROM node:20-bookworm-slim AS frontend-build
|
||||
WORKDIR /frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm install --no-audit --no-fund
|
||||
|
||||
# Copy sources and build
|
||||
COPY . .
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# Use a lightweight nginx to serve the client build output
|
||||
FROM nginx:alpine AS runner
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
########################
|
||||
# 2) Backend build #
|
||||
########################
|
||||
FROM node:20-bookworm-slim AS backend-build
|
||||
WORKDIR /app
|
||||
COPY backend/package*.json ./
|
||||
RUN npm install --no-audit --no-fund
|
||||
COPY backend/ .
|
||||
# Copy compiled frontend into backend/public (served statically by Nest)
|
||||
COPY --from=frontend-build /frontend/dist ./public
|
||||
RUN npm run build
|
||||
|
||||
# Default nginx port
|
||||
EXPOSE 80
|
||||
########################
|
||||
# 3) Production image #
|
||||
########################
|
||||
FROM node:20-bookworm-slim AS runner
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# Install only production deps (reuse original package.json)
|
||||
COPY backend/package*.json ./
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
|
||||
# Copy backend dist + public assets
|
||||
COPY --from=backend-build /app/dist ./dist
|
||||
COPY --from=backend-build /app/public ./public
|
||||
|
||||
# Environment (override at runtime as needed)
|
||||
ENV PORT=3000 \
|
||||
SERIAL_BAUD=115200
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
services:
|
||||
frontend:
|
||||
build: .
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: frontend/Dockerfile
|
||||
container_name: schafkop-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
# Optionally set SERIAL_PORT or PICO_SERIAL_PORT for auto-open
|
||||
# - SERIAL_PORT=/dev/serial0
|
||||
# - SERIAL_BAUD=115200
|
||||
ports:
|
||||
- "80:80"
|
||||
- "80:3000"
|
||||
# Uncomment and adjust one of the following for hardware access on a Pi:
|
||||
# devices:
|
||||
# - /dev/serial0:/dev/serial0
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
|
||||
697
frontend/package-lock.json
generated
697
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,11 @@
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"svelte": "^5.38.1",
|
||||
"svelte-check": "^4.3.1",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
let es: EventSource | null = null;
|
||||
|
||||
function addLog(line: string) {
|
||||
log.update((l) => [new Date().toISOString() + ' ' + line, ...l].slice(0, 200));
|
||||
// keep a fairly large history and prefix with an ISO timestamp
|
||||
log.update((l) => [new Date().toISOString() + ' ' + line, ...l].slice(0, 2000));
|
||||
}
|
||||
|
||||
async function list() {
|
||||
@@ -26,14 +27,18 @@
|
||||
const res = await fetch('/serial/open', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }) });
|
||||
const j = await res.json().catch(() => null);
|
||||
console.log('[serial] openPort result', j);
|
||||
// refresh status
|
||||
// log result and refresh status
|
||||
if (j?.ok) addLog('PORT: open requested ' + path);
|
||||
else addLog('PORT: open request failed - ' + (j?.error ?? 'unknown'));
|
||||
setTimeout(getStatus, 300);
|
||||
}
|
||||
|
||||
async function closePort() {
|
||||
console.log('[serial] closePort()');
|
||||
const res = await fetch('/serial/close', { method: 'POST' });
|
||||
console.log('[serial] closePort result', await res.json().catch(() => null));
|
||||
const j = await res.json().catch(() => null);
|
||||
console.log('[serial] closePort result', j);
|
||||
addLog('PORT: close requested');
|
||||
}
|
||||
|
||||
async function sendLine(txt: string) {
|
||||
@@ -61,13 +66,17 @@
|
||||
if (es) return;
|
||||
console.log('[serial] startStream()');
|
||||
es = new EventSource('/serial/stream');
|
||||
es.addEventListener('open', () => { console.log('[serial][SSE] open'); connected.set(true); });
|
||||
es.addEventListener('close', () => { console.log('[serial][SSE] close'); connected.set(false); });
|
||||
es.addEventListener('error', (e: any) => { console.warn('[serial][SSE] error', e); addLog('SSE error'); });
|
||||
// connection lifecycle events - also log them so the serial box shows open/close/errors
|
||||
es.addEventListener('open', () => { console.log('[serial][SSE] open'); connected.set(true); addLog('[SSE] connected'); });
|
||||
es.addEventListener('close', () => { console.log('[serial][SSE] close'); connected.set(false); addLog('[SSE] disconnected'); });
|
||||
es.addEventListener('error', (e: any) => { console.warn('[serial][SSE] error', e); addLog('[SSE] error: ' + (e?.message ?? JSON.stringify(e))); });
|
||||
|
||||
// data events contain the actual lines emitted by the Pico; parse JSON payloads but fall back to raw
|
||||
es.addEventListener('data', (ev: any) => {
|
||||
console.log('[serial][SSE] data event', ev.data);
|
||||
try {
|
||||
const d = JSON.parse(ev.data);
|
||||
// always show the raw line emitted by the firmware
|
||||
addLog('RX: ' + d.line);
|
||||
} catch (e) {
|
||||
addLog('RX (raw): ' + ev.data);
|
||||
@@ -83,6 +92,10 @@
|
||||
}
|
||||
|
||||
let statusInterval: any;
|
||||
// quick test command defaults
|
||||
let stepVal: string = '4096';
|
||||
let dirVal: string = '1';
|
||||
let speedVal: string = '3000';
|
||||
onMount(() => {
|
||||
list();
|
||||
startStream();
|
||||
@@ -93,43 +106,109 @@
|
||||
stopStream();
|
||||
clearInterval(statusInterval);
|
||||
});
|
||||
|
||||
function sendStepCmd() {
|
||||
const steps = Number(stepVal);
|
||||
const dir = Number(dirVal) === 1 ? 1 : 0;
|
||||
if (!Number.isFinite(steps) || steps <= 0) {
|
||||
addLog('TX error: invalid STEP value: ' + stepVal);
|
||||
return;
|
||||
}
|
||||
const line = `STEP ${steps} ${dir}`;
|
||||
sendLine(line);
|
||||
addLog('TX: ' + line);
|
||||
}
|
||||
|
||||
function sendSpeedCmd() {
|
||||
const d = Number(speedVal);
|
||||
if (!Number.isFinite(d) || d <= 0) {
|
||||
addLog('TX error: invalid SPEED value: ' + speedVal);
|
||||
return;
|
||||
}
|
||||
const line = `SPEED ${d}`;
|
||||
sendLine(line);
|
||||
addLog('TX: ' + line);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Raspberry Pico — Serial Monitor</h1>
|
||||
<main class="min-h-screen bg-gradient-to-b from-bg to-surface text-gray-100 p-8">
|
||||
<h1 class="text-3xl font-bold underline">
|
||||
Hello world!
|
||||
</h1>
|
||||
<div class="container mx-auto">
|
||||
<header class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Raspberry Pico</h1>
|
||||
<p class="text-sm text-gray-400">Serial monitor & command tester</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-gray-300">SSE: {$connected ? 'connected' : 'disconnected'}</span>
|
||||
<button class="px-3 py-1 bg-primary text-white rounded-md text-sm" on:click={startStream}>Start</button>
|
||||
<button class="px-3 py-1 bg-gray-700 text-white rounded-md text-sm" on:click={stopStream}>Stop</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h3>Available ports</h3>
|
||||
<button on:click={list}>Refresh</button>
|
||||
<ul>
|
||||
{#each $ports as p}
|
||||
<li>
|
||||
<code>{p.path}</code>
|
||||
<button on:click={() => openPort(p.path)}>Open</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<button on:click={closePort}>Close port</button>
|
||||
</section>
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<aside class="col-span-4 bg-gray-900/60 p-4 rounded-lg border border-gray-800">
|
||||
<section class="mb-4">
|
||||
<h3 class="text-sm text-gray-300 font-medium">Available ports</h3>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<button class="px-2 py-1 text-sm bg-gray-700 rounded" on:click={list}>Refresh</button>
|
||||
<button class="px-2 py-1 text-sm bg-red-600 rounded" on:click={closePort}>Close port</button>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-2">
|
||||
{#each $ports as p}
|
||||
<li class="flex items-center justify-between bg-gray-800/40 p-2 rounded">
|
||||
<code class="text-xs text-gray-200">{p.path}</code>
|
||||
<button class="px-2 py-1 text-sm bg-primary rounded" on:click={() => openPort(p.path)}>Open</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Send</h3>
|
||||
<div style="display:flex; align-items:center; gap:1rem;">
|
||||
<div>Port: {$portOpen ? 'open' : 'closed'}</div>
|
||||
<SendBox disabled={!$portOpen} on:send={(e) => { sendLine(e.detail); addLog('TX: ' + e.detail); }} />
|
||||
<section class="mb-4">
|
||||
<h3 class="text-sm text-gray-300 font-medium">Send</h3>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<div class="text-xs text-gray-300">Port: <span class="font-medium">{$portOpen ? 'open' : 'closed'}</span></div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<SendBox disabled={!$portOpen} on:send={(e) => { sendLine(e.detail); addLog('TX: ' + e.detail); }} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-sm text-gray-300 font-medium">Quick commands</h3>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<input class="w-28 p-2 rounded bg-gray-800 text-sm" type="number" bind:value={stepVal} min="1" />
|
||||
<select class="p-2 rounded bg-gray-800 text-sm" bind:value={dirVal}>
|
||||
<option value="1">1 (forward)</option>
|
||||
<option value="0">0 (reverse)</option>
|
||||
</select>
|
||||
<button class="px-3 py-1 bg-primary text-white rounded" on:click={sendStepCmd} disabled={!$portOpen}>Send STEP</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
<input class="w-28 p-2 rounded bg-gray-800 text-sm" type="number" bind:value={speedVal} min="1" />
|
||||
<button class="px-3 py-1 bg-primary text-white rounded" on:click={sendSpeedCmd} disabled={!$portOpen}>Send SPEED</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<section class="col-span-8 bg-gray-900/60 p-4 rounded-lg border border-gray-800 flex flex-col">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm text-gray-300 font-medium">Live log</h3>
|
||||
<div class="text-xs text-gray-400">Showing last {2000} lines</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto bg-[#0b0b0b] p-3 rounded text-sm font-mono text-gray-100">
|
||||
{#each $log as l}
|
||||
<div class="py-0.5"><code class="text-xs">{l}</code></div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Live log ({$connected ? 'connected' : 'disconnected'})</h3>
|
||||
<button on:click={startStream}>Start stream</button>
|
||||
<button on:click={stopStream}>Stop stream</button>
|
||||
<div style="height:320px; overflow:auto; background:#111; color:#dfe6ef; padding:8px; border-radius:6px;">
|
||||
{#each $log as l}
|
||||
<div><code>{l}</code></div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="margin-top:0.5rem; display:flex; gap:0.5rem; align-items:center;">
|
||||
<input bind:value={txt} placeholder={placeholder} on:keydown={(e) => e.key === 'Enter' && doSend()} style="flex:1; padding:0.4rem;" disabled={disabled} />
|
||||
<button on:click={doSend} disabled={disabled}>Send</button>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2 items-center">
|
||||
<input class="flex-1 p-2 rounded bg-gray-800 text-sm text-gray-100" bind:value={txt} placeholder={placeholder} on:keydown={(e) => e.key === 'Enter' && doSend()} disabled={disabled} />
|
||||
<button class="px-3 py-1 bg-primary text-white rounded text-sm" on:click={doSend} disabled={disabled}>Send</button>
|
||||
</div>
|
||||
|
||||
@@ -30,3 +30,15 @@ a:hover { opacity: 0.9; }
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root { --bg: #fafafa; --surface:#fff; --text:#111827; --muted:#556; }
|
||||
}
|
||||
|
||||
|
||||
/* small helpers that complement tailwind tokens */
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 1.25rem; }
|
||||
|
||||
:focus { outline: 3px solid rgba(124,108,255,0.18); outline-offset: 2px; }
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "tailwindcss";
|
||||
@@ -1,9 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
plugins: [
|
||||
svelte(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
// forward serial API calls (including SSE) to backend running on :3000
|
||||
|
||||
Reference in New Issue
Block a user