This commit is contained in:
2026-02-28 07:57:40 +01:00
parent bed9ab1968
commit 8a124fe17d
17 changed files with 1186 additions and 70 deletions

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { updateDeviceInfo, refreshFileList, setActivePort } from '../lib/buzzerActions';
import { set } from 'astro:schema';
let port: SerialPort | null = null;
const filters = [{ usbVendorId: 0x2fe3, usbProductId: 0x0001 }];
async function connect() {
try {
port = await navigator.serial.requestPort({ filters });
await port.open({ baudRate: 115200 });
// Store aktualisieren
buzzer.update(b => ({ ...b, connected: true }));
if (port) {
setActivePort(port); // Aktiven Port in den Actions setzen
await updateDeviceInfo(port);
await refreshFileList(port);
}
} catch (e) {
setActivePort(null); // Port zurücksetzen
console.error("Verbindung fehlgeschlagen:", e);
buzzer.update(b => ({ ...b, connected: false }));
}
}
async function disconnect() {
if (port) {
await port.close();
port = null;
buzzer.update(b => ({ ...b, connected: false }));
}
}
</script>
<button
on:click={$buzzer.connected ? disconnect : connect}
class="flex items-center gap-2 px-6 py-2 {$buzzer.connected ? 'bg-blue-600 hover:bg-blue-500' : 'bg-emerald-600 hover:bg-emerald-500'} text-white text-xs font-black rounded-full transition-all shadow-lg active:scale-95 uppercase tracking-widest"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
{#if $buzzer.connected}
<rect width="256" height="256" fill="none"/>
<rect x="63.03" y="88.4" width="129.94" height="79.2" rx="24" transform="translate(-53.02 128) rotate(-45)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="88" y1="88" x2="168" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="232" y1="24" x2="173.94" y2="82.06" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="82.06" y1="173.94" x2="24" y2="232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
{:else}
<rect width="256" height="256" fill="none"/>
<line x1="144" y1="144" x2="120" y2="168" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<path d="M132,180l-29,29a24,24,0,0,1-33.94,0L47,186.91A24,24,0,0,1,47,153l29-29" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<line x1="197.94" y1="58.06" x2="232" y2="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
<path d="M180,132l29-29a24,24,0,0,0,0-33.94L186.91,47A24,24,0,0,0,153,47L124,76" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/>
{/if}
</svg>
{$buzzer.connected ? 'Disconnect' : 'Connect'}
</button>

View File

@@ -0,0 +1,17 @@
<script>
import { buzzer } from '../lib/buzzerStore';
</script>
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10">
<p>
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
</p>
<p>
Protocol: <span class="text-indigo-300">{$buzzer.protocol}</span>
</p>
<p>
Status: <span class="{$buzzer.connected ? 'text-emerald-400' : 'text-red-400'}">
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
</span>
</p>
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
$: storage = $buzzer.storage;
$: total = storage.total > 0 ? storage.total : 8.0;
// Berechnung der Prozentanteile
$: pAudio = (storage.usedAudio / total) * 100;
$: pSys = (storage.usedSys / total) * 100;
$: pMeta = (storage.unknown / total) * 100;
$: pFree = (storage.available / total) * 100;
// Zustand für das UI
$: isDisconnected = !$buzzer.connected;
</script>
<div class="flex flex-col gap-1.5 w-96 transition-all duration-700 {isDisconnected ? 'blur-sm opacity-30 grayscale pointer-events-none' : ''}">
<div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner">
<div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>
<div class="h-full bg-fuchsia-500 transition-all duration-500" style="width: {pSys}%"></div>
<div class="h-full bg-blue-500 transition-all duration-500" style="width: {pAudio}%"></div>
<div class="h-full bg-emerald-500 transition-all duration-500" style="width: {pFree}%"></div>
</div>
<div class="text-[9px] text-slate-400 flex justify-between uppercase tracking-[0.2em] font-black">
<div class="flex gap-3">
<span class={pMeta > 0 ? "text-slate-300" : "text-slate-600"}>Meta</span>
<span class={pSys > 0 ? "text-fuchsia-400" : "text-slate-600"}>Sys</span>
<span class={pAudio > 0 ? "text-blue-400" : "text-slate-600"}>Audio</span>
</div>
<span class="text-emerald-400">
{isDisconnected ? 'Offline' : storage.available.toFixed(2) + ' MB Free'}
</span>
</div>
</div>

View File

@@ -0,0 +1,31 @@
---
// src/components/FileRow.astro
import { Icon } from 'astro-icon/components';
const { name, size, selected = false, isSystem = false } = Astro.props;
const activeClasses = selected
? "bg-blue-600/20 border-l-4 border-blue-500 shadow-[inset_0_0_20px_rgba(59,130,246,0.1)]"
: "hover:bg-slate-700/40 border-l-4 border-transparent";
---
<div class={`flex items-center justify-between p-3 cursor-pointer transition-all group border-b border-slate-700/30 ${activeClasses}`}>
<div class="flex items-center gap-4">
<div class={`text-2xl ${selected ? 'text-blue-400' : 'text-slate-500'}`}>
{isSystem ? <Icon name="ph:wrench-fill" /> : <Icon name="ph:music-notes-fill" />}
</div>
<div class="flex flex-col overflow-hidden">
<span class={`text-sm truncate ${selected ? 'font-bold text-white' : 'text-slate-200'}`}>{name}</span>
<span class="text-[10px] text-slate-500 font-mono tracking-tighter">{size}</span>
</div>
</div>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors">
<Icon name="ph:play-fill" class="w-4 h-4" />
</button>
<button class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors">
<Icon name="ph:trash-fill" class="w-4 h-4" />
</button>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions';
// Die Datei-Daten werden vom Parent (FileStorage) übergeben
export let file: { name: string, size: string, isSystem: boolean };
export let selected = false;
// Wir benötigen den Port für die Kommandos
// In einem echten Szenario würden wir den Port in einem Store speichern.
// Hier nehmen wir an, er wird über die Actions gehandelt.
</script>
<div
class="flex items-center justify-between p-3 cursor-pointer transition-all group border-b border-slate-700/30
{selected ? 'bg-blue-600/20 border-l-4 border-blue-500 shadow-[inset_0_0_20px_rgba(59,130,246,0.1)]' : 'hover:bg-slate-700/40 border-l-4 border-transparent'}"
>
<div class="flex items-center gap-4 overflow-hidden">
<div class="text-2xl {selected ? 'text-blue-400' : 'text-slate-500'} shrink-0">
{#if file.isSystem}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M225.9,103.11l-40.45,40.44a8,8,0,0,1-11.31,0L148,117.41a8,8,0,0,1,0-11.31l40.44-40.45a72,72,0,0,0-85.3,109.11l-51.5,51.5a24,24,0,0,0,33.94,33.94l51.5-51.5A72,72,0,0,0,225.9,103.11ZM160,80a8,8,0,1,1-8-8A8,8,0,0,1,160,80Z"></path></svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M210.3,56.34l-80-24A8,8,0,0,0,120,40V148.26A48,48,0,1,0,136,184V98.71l69.7,20.91a8,8,0,0,0,10.3-7.62V64A8,8,0,0,0,210.3,56.34ZM88,216a32,32,0,1,1,32-32A32,32,0,0,1,88,216Zm112-112.71L136,83.89V49.11L200,68.31Z"></path></svg>
{/if}
</div>
<div class="flex flex-col overflow-hidden">
<span class="text-sm truncate {selected ? 'font-bold text-white' : 'text-slate-200'}">{file.name}</span>
<span class="text-[10px] text-slate-500 font-mono tracking-tighter uppercase">{file.size}</span>
</div>
</div>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
on:click|stopPropagation={() => playFile(file.name)}
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
title="Play Sound"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.75a16,16,0,0,1-16.2,0A15.86,15.86,0,0,1,64,216.24V39.76a15.86,15.86,0,0,1,8.12-13.51,16,16,0,0,1,16.2,0L232.4,114.49A15.74,15.74,0,0,1,240,128Z"></path></svg>
</button>
<button
on:click|stopPropagation={() => deleteFile(file.name)}
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
title="Delete File"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
</button>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte';
async function handleRefresh() {
// Falls wir den Port im Store haben, hier nutzen
// ansonsten wird er über die Actions verwaltet
await refreshFileList();
}
</script>
<div class="flex-1 bg-slate-800/50 rounded-[2rem] border border-slate-700 flex flex-col overflow-hidden shadow-2xl relative">
<div class="p-5 border-b border-slate-700 bg-slate-800/80 flex justify-between items-center shrink-0">
<h2 class="font-black text-[10px] uppercase tracking-[0.2em] text-slate-500">
Flash Storage <span class="text-slate-600">/LFS/A</span>
</h2>
<button
on:click={handleRefresh}
class="text-slate-500 hover:text-blue-400 transition-colors active:rotate-180 duration-500">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none"/><polyline points="88 96 40 96 40 48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M40,96,68.28,67.72A88,88,0,0,1,192,67" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="168 160 216 160 216 208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,160l-28.28,28.28A88,88,0,0,1,64,189" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/> </svg>
</button>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar relative z-10">
{#if $buzzer.connected}
<div class="flex flex-col">
{#each $buzzer.files as file}
<FileRow {file} />
{:else}
<div class="p-10 text-center text-slate-600 text-[10px] uppercase tracking-widest italic">
Keine Dateien auf dem Buzzer
</div>
{/each}
</div>
{:else}
<div class="flex-1 flex items-center justify-center p-10">
<span class="text-slate-700 text-[10px] font-black uppercase tracking-[0.3em] animate-pulse">
Offline - Warte auf Port
</span>
</div>
{/if}
</div>
<div class="absolute bottom-6 right-6 text-slate-700 opacity-5 pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" fill="currentColor" viewBox="0 0 256 256">
<path d="M208,40V216a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V40A16,16,0,0,1,64,24H192A16,16,0,0,1,208,40Z" opacity="0.2"></path>
<path d="M208,40V216a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V40A16,16,0,0,1,64,24H192A16,16,0,0,1,208,40Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></path>
<line x1="80" y1="24" x2="80" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
<line x1="128" y1="24" x2="128" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
<line x1="176" y1="24" x2="176" y2="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"></line>
</svg>
</div>
</div>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import { onMount } from 'svelte';
let unsupported = true;
onMount(() => {
if (typeof navigator !== 'undefined' && 'serial' in navigator) {
unsupported = false;
}
});
</script>
{#if unsupported}
<div class="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-slate-950/40 backdrop-blur-2xl transition-all">
<div class="w-full max-w-md bg-slate-900 border-2 border-red-500/30 rounded-[3rem] p-12 text-center shadow-2xl">
<div class="flex justify-center text-slate-400 text500 mb-8">
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="currentColor" viewBox="0 0 256 256">
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216ZM164,108a12,12,0,1,1-12-12A12,12,0,0,1,164,108ZM92,120a12,12,0,1,1,12-12A12,12,0,0,1,92,120Zm71.74,48a8,8,0,0,1-11.48,1.26,44,44,0,0,0-56.52,0,8,8,0,1,1-10.22-12.31,60,60,0,0,1,77,0A8,8,0,0,1,163.74,168Z"></path>
</svg>
</div>
<h2 class="text-2xl font-black text-white uppercase tracking-tight mb-4">Browser Check</h2>
<p class="text-slate-400 text-sm leading-relaxed mb-8">
Diese App benötigt die <span class="text-blue-400 font-mono">Web Serial API</span>, um direkt mit dem Buzzer zu interagieren.
Dein Browser scheint das nicht zu unterstützen.
</p>
<div class="flex flex-col gap-3">
<a href="https://www.google.com/chrome/" target="_blank"
class="block w-full py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl text-xs font-bold uppercase tracking-[0.2em] transition-all">
Chrome nutzen
</a>
<p class="text-[10px] text-slate-500 uppercase tracking-widest font-medium italic">
Dieses komische Winzigweich Kante geht wohl auch...
</p>
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,47 @@
---
// src/components/StatusModal.astro
import { Icon } from 'astro-icon/components';
const { visible = false, currentFile = "firmware.bin", progress = 45, totalProgress = 20 } = Astro.props;
---
{visible && (
<div class="fixed inset-0 z-50 flex items-center justify-center p-6 bg-slate-950/80 backdrop-blur-sm">
<div class="w-full max-w-lg bg-slate-800 border border-slate-700 rounded-[2.5rem] p-8 shadow-2xl">
<div class="flex items-center gap-4 mb-8">
<div class="p-3 bg-blue-500/20 rounded-2xl text-blue-400 animate-pulse">
<Icon name="ph:upload-simple-fill" class="w-8 h-8" />
</div>
<div>
<h2 class="text-xl font-bold text-white">Transferring Data</h2>
<p class="text-sm text-slate-400">Please do not disconnect the device</p>
</div>
</div>
<div class="space-y-6">
<div>
<div class="flex justify-between text-xs mb-2">
<span class="text-slate-300 font-mono">{currentFile}</span>
<span class="text-blue-400 font-bold">{progress}%</span>
</div>
<div class="h-2 w-full bg-slate-900 rounded-full overflow-hidden">
<div class="h-full bg-blue-500 transition-all" style={`width: ${progress}%`}></div>
</div>
</div>
<div>
<div class="flex justify-between text-xs mb-2 italic text-slate-500">
<span>Overall Progress</span>
<span>{totalProgress}%</span>
</div>
<div class="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
<div class="h-full bg-indigo-500 transition-all" style={`width: ${totalProgress}%`}></div>
</div>
</div>
</div>
<button class="mt-10 w-full py-3 bg-slate-700 hover:bg-red-900/40 hover:text-red-400 rounded-2xl text-xs font-bold uppercase tracking-widest transition-all">
Abort Transfer
</button>
</div>
</div>
)}

View File

@@ -0,0 +1,123 @@
// src/lib/buzzerActions.ts
import { buzzer } from './buzzerStore';
let activePort: SerialPort | null = null;
/**
* Hilfsfunktion für die Kommunikation
*/
async function sendCommand(port: SerialPort, command: string): Promise<string[]> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const writer = port.writable.getWriter();
const reader = port.readable.getReader();
try {
await writer.write(encoder.encode(command + "\n"));
writer.releaseLock();
let raw = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
raw += decoder.decode(value);
if (raw.includes("OK")) break;
}
return raw.split('\n').map(l => l.trim()).filter(l => l && l !== "OK" && !l.startsWith(command));
} finally {
reader.releaseLock();
}
}
// WICHTIG: Hier muss "export" stehen!
export async function updateDeviceInfo(port: SerialPort) {
const lines = await sendCommand(port, "info");
if (lines.length > 0) {
const parts = lines[0].split(';');
if (parts.length >= 6) {
const pageSize = parseInt(parts[2]);
const totalPages = parseInt(parts[3]);
const availablePages = parseInt(parts[4]);
const totalMB = (totalPages * pageSize) / (1024 * 1024);
const availableMB = (availablePages * pageSize) / (1024 * 1024);
buzzer.update(s => ({
...s,
version: parts[1],
protocol: parseInt(parts[0]),
storage: { ...s.storage, total: totalMB, available: availableMB }
}));
}
}
}
// WICHTIG: Auch hier "export"
export async function refreshFileList(port: SerialPort) {
if (!port) return;
let audioSize = 0;
let sysSize = 0;
const audioFiles: {name: string, size: string, crc32: number, isSystem: boolean}[] = [];
async function scanDir(path: string) {
const lines = await sendCommand(port, `ls ${path}`);
for (const line of lines) {
const parts = line.split(',');
if (parts.length < 3) continue;
const [type, sizeStr, name] = parts;
const size = parseInt(sizeStr);
const fullPath = `${path}/${name}`;
if (type === 'D') {
await scanDir(fullPath);
} else if (type === 'F') {
if (path.startsWith('/lfs/a')) {
audioSize += size;
console.log("Audio-Datei gefunden:", name);
audioFiles.push({ name, size: (size / 1024).toFixed(1) + " KB", crc32: 0, isSystem: false });
} else if (path.startsWith('/lfs/sys')) {
sysSize += size;
}
}
}
}
await scanDir('/lfs');
buzzer.update(s => {
const bytesToMB = 1024 * 1024;
const usedMB = s.storage.total - s.storage.available;
const audioMB = audioSize / bytesToMB;
const sysMB = sysSize / bytesToMB;
const unknownMB = usedMB - audioMB - sysMB;
return {
...s,
files: audioFiles,
storage: { ...s.storage, usedAudio: audioMB, usedSys: sysMB, unknown: unknownMB > 0 ? unknownMB : 0 }
};
});
}
export function setActivePort(port: SerialPort) {
activePort = port;
}
export async function playFile(filename: string) {
if (!activePort) {
console.warn("Kein aktiver Port zum Abspielen der Datei.");
return;
}
console.log(`Starte Wiedergabe: ${filename}`);
// Kommando: play /lfs/a/filename
await sendCommand(activePort, `play /lfs/a/${filename}`);
}
export async function deleteFile(filename: string) {
if (!activePort || !confirm(`Datei ${filename} wirklich löschen?`)) return;
// Kommando: rm /lfs/a/filename
await sendCommand(activePort, `rm /lfs/a/${filename}`);
// Liste nach dem Löschen aktualisieren
await refreshFileList(activePort);
}

View File

@@ -0,0 +1,16 @@
import { writable } from 'svelte/store';
export const buzzer = writable({
connected: false,
version: 'v0.0.0',
protocol: 0,
build: 'unknown',
storage: {
total: 8.0, // 8 MB Flash laut Spezifikation
unknown: 8.0,
available: 0.0,
usedSys: 0.0,
usedAudio: 0.0
},
files: [] as {name: string, size: string, crc32: number, isSystem: boolean}[]
});

View File

@@ -1,73 +1,95 @@
---
import "../styles/global.css";
import DiskUsage from '../components/DiskUsage.astro';
import FileRow from '../components/FileRow.astro';
import { Icon } from "astro-icon/components";
import DiskUsage from "../components/DiskUsage.svelte";
import FileRow from "../components/FileRow.astro";
import StatusModal from "../components/StatusModal.astro";
import ConnectButton from "../components/ConnectButton.svelte";
import SerialWarning from "../components/SerialWarning.svelte";
import FileStorage from "../components/FileStorage.svelte";
import DeviceInfo from "../components/DeviceInfo.svelte";
---
<html lang="de">
<head>
<meta charset="utf-8" />
<title>Buzzer Dashboard v2</title>
</head>
<body class="bg-slate-900 selection:bg-blue-500/30">
<div class="min-h-screen max-w-[1600px] min-w-[1024px] mx-auto text-slate-100 flex flex-col font-sans">
<header class="h-16 border-b border-slate-700 bg-slate-800 flex items-center justify-between px-8 shrink-0 shadow-lg">
<div class="flex items-center gap-4">
<div class="h-3 w-3 rounded-full bg-emerald-500 shadow-[0_0_10px_#10b981] animate-pulse"></div>
<h1 class="font-bold tracking-tighter text-xl italic">EDI_BUZZER <span class="text-slate-600 not-italic text-xs font-mono ml-2">PROT_V2</span></h1>
</div>
<DiskUsage />
<head>
<meta charset="utf-8" />
<title>Buzzer Dashboard v2</title>
</head>
<body class="bg-slate-900 selection:bg-blue-500/30">
<SerialWarning client:load />
<StatusModal visible={false} />
<button class="px-6 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-black rounded-full transition-all shadow-lg hover:shadow-emerald-900/40 active:scale-95 uppercase tracking-widest">
Connect
</button>
</header>
<div class="min-h-screen max-w-[1600px] min-w-[1024px] mx-auto text-slate-100 flex flex-col font-sans">
<header
class="h-16 border-b border-slate-700 bg-slate-800 grid grid-cols-3 items-center px-8 shrink-0 shadow-lg">
<div class="flex items-center gap-4">
<div class="h-3 w-3 rounded-full bg-emerald-500 shadow-[0_0_10px_#10b981] animate-pulse"></div>
<h1 class="font-bold tracking-tighter text-xl italic text-white uppercase">
EDIs_BUZZER <span class="text-slate-600 not-italic text-xs font-mono ml-2">PROT_V2</span>
</h1>
</div>
<main class="flex-1 flex overflow-hidden p-6 gap-6 bg-gradient-to-b from-slate-900 to-slate-950">
<section class="flex-1 flex flex-col gap-6">
<div class="h-48 bg-slate-800/30 border-2 border-dashed border-slate-700 rounded-[2rem] flex flex-col items-center justify-center group hover:border-blue-500/40 transition-all cursor-pointer">
<span class="text-slate-500 font-bold uppercase text-[10px] tracking-[0.2em] group-hover:text-blue-400 transition-colors">Drop PC Files</span>
</div>
<div class="flex-1 bg-slate-800/50 rounded-[2rem] border border-slate-700 flex flex-col overflow-hidden shadow-2xl">
<div class="p-5 border-b border-slate-700 bg-slate-800/80 font-black text-[10px] uppercase tracking-[0.2em] text-slate-500">Local Queue</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<FileRow name="Intro_Song.mp3" size="1.2 MB" />
<FileRow name="Buzzer_Hit.wav" size="450 KB" selected={true} />
</div>
</div>
</section>
<div class="justify-self-center">
<DiskUsage client:load />
</div>
<section class="w-16 flex flex-col items-center justify-center gap-8">
<button class="w-14 h-14 bg-blue-600 rounded-2xl shadow-xl hover:bg-blue-500 active:scale-90 transition-all flex items-center justify-center text-2xl">⮕</button>
<button class="w-14 h-14 bg-slate-700 rounded-2xl shadow-xl hover:bg-slate-600 active:scale-90 transition-all flex items-center justify-center text-2xl">⬅</button>
</section>
<div class="justify-self-end">
<ConnectButton client:load />
</div>
</header>
<section class="flex-1 flex flex-col gap-6">
<div class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6">
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">Device Info</h3>
<div class="text-[11px] font-mono text-slate-400 space-y-1">
<p>Firmware: <span class="text-indigo-300">v0.1.12</span></p>
<p>Build: <span class="text-indigo-300">NCS 3.2.1</span></p>
</div>
</div>
<div class="flex-1 bg-slate-800/50 rounded-[2rem] border border-slate-700 flex flex-col overflow-hidden shadow-2xl">
<div class="p-5 border-b border-slate-700 bg-slate-800/80 font-black text-[10px] uppercase tracking-[0.2em] text-slate-500">Flash Storage /lfs/a</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<FileRow name="confirm.bin" size="12 KB" isSystem={true} />
<FileRow name="test_sound.bin" size="142 KB" />
</div>
</div>
</section>
<main class="flex-1 flex overflow-hidden p-6 gap-6 bg-gradient-to-b from-slate-900 to-slate-950">
<section class="flex-1 flex flex-col gap-6">
<div
class="h-48 bg-slate-800/30 border-2 border-dashed border-slate-700 rounded-[2rem] flex flex-col items-center justify-center group hover:border-blue-500/40 transition-all cursor-pointer">
<Icon
name="ph:cloud-arrow-up-fill"
class="text-4xl text-slate-700 group-hover:text-blue-500 mb-2 transition-colors"
/>
<span
class="text-slate-500 font-bold uppercase text-[10px] tracking-[0.2em] group-hover:text-blue-400 transition-colors"
>Drop PC Files</span
>
</div>
<div
class="flex-1 bg-slate-800/50 rounded-[2rem] border border-slate-700 flex flex-col overflow-hidden shadow-2xl">
<div
class="p-5 border-b border-slate-700 bg-slate-800/80 font-black text-[10px] uppercase tracking-[0.2em] text-slate-500">
Local Queue
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar">
<FileRow name="Intro_Song.mp3" size="1.2 MB" />
<FileRow name="Buzzer_Hit.wav" size="450 KB" selected={true} />
</div>
</div>
</section>
</main>
<section class="flex-1 flex flex-col gap-6 min-h-0">
<div
class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
<Icon name="ph:cpu-fill" class="w-12 h-12" />
</div>
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
Device Info
</h3>
<DeviceInfo client:load />
</div>
<footer class="h-10 bg-black/40 border-t border-slate-800 px-8 flex items-center justify-between font-mono text-[9px] text-emerald-500/50 uppercase tracking-widest">
<span>Status: Idle</span>
<span class="text-slate-700 italic font-sans">Protocol V2.0 Active</span>
</footer>
</div>
</body>
</html>
<div class="flex-1 flex flex-col overflow-hidden">
<FileStorage client:load />
</div>
</section>
</main>
<footer
class="h-10 bg-black/40 border-t border-slate-800 px-8 flex items-center justify-between font-mono text-[9px] text-emerald-500/50 uppercase tracking-widest">
<div class="flex items-center gap-2">
<Icon name="ph:terminal-window-fill" class="w-4 h-4" />
<span>Status: Idle</span>
</div>
<span class="text-slate-700 italic font-sans">Protocol V2.0 Active</span>
</footer>
</div>
</body>
</html>