chore: reorganise frontend code

This commit is contained in:
Suyog Tandel
2026-01-12 01:01:56 +05:30
parent 5f2d13710a
commit a9db653787
18 changed files with 1032 additions and 958 deletions
@@ -0,0 +1,22 @@
<script lang="ts">
// import { Button } from "$lib/components/ui/button";
// import { Input } from "$lib/components/ui/input";
// import { Label } from "$lib/components/ui/label";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as state } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={state.dialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{state.dialogTitle}</AlertDialog.Title>
<AlertDialog.Description>
{state.dialogMessage}
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Action onclick={() => (state.dialogOpen = false)}>Okay</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
@@ -0,0 +1,45 @@
<script lang="ts">
// import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Slider } from "$lib/components/ui/slider/index.js";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as state } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={state.minPinDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Update Minimum PIN Length</AlertDialog.Title>
<AlertDialog.Description>
Set the minimum allowed PIN length (4-63 characters) and enter a new PIN that meets this requirement.
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="min-pin-length">Minimum PIN Length ({state.minPinLength})</Label>
<Slider type="single" bind:value={state.minPinLength} min={4} max={63} step={1} />
</div>
<div class="space-y-2">
<Label for="min-pin-current">Current PIN</Label>
<Input id="min-pin-current" type="password" bind:value={state.minPinCurrentPin} placeholder="Enter current PIN" />
</div>
<div class="space-y-2">
<Label for="min-pin-new">New PIN (min {state.minPinLength} chars)</Label>
<Input id="min-pin-new" type="password" bind:value={state.minPinNewPin} placeholder="Enter new PIN" />
</div>
<div class="space-y-2">
<Label for="min-pin-confirm">Confirm New PIN</Label>
<Input id="min-pin-confirm" type="password" bind:value={state.minPinConfirmPin} placeholder="Confirm new PIN" />
</div>
{#if state.minPinError}
<p class="text-sm text-destructive">{state.minPinError}</p>
{/if}
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={() => (state.minPinDialogOpen = false)}>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={state.handleMinPinChange}>Update</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
@@ -0,0 +1,44 @@
<script lang="ts">
// import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as AlertDialog from "$lib/components/ui/alert-dialog";
import { configViewState as state } from "$lib/state/configState.svelte";
</script>
<AlertDialog.Root bind:open={state.setPinDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>{state.isSettingPin ? "Set PIN" : "Change PIN"}</AlertDialog.Title>
<AlertDialog.Description>
{state.isSettingPin
? "Set a PIN for your FIDO2 device. Minimum length is " + (state.minPinLength || 4) + " characters."
: "Enter your current PIN and choose a new one."}
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
{#if !state.isSettingPin}
<div class="space-y-2">
<Label for="current-pin">Current PIN</Label>
<Input id="current-pin" type="password" bind:value={state.currentPin} placeholder="Enter current PIN" />
</div>
{/if}
<div class="space-y-2">
<Label for="new-pin">New PIN</Label>
<Input id="new-pin" type="password" bind:value={state.newPin} placeholder="Enter new PIN" />
</div>
<div class="space-y-2">
<Label for="confirm-pin">Confirm New PIN</Label>
<Input id="confirm-pin" type="password" bind:value={state.confirmPin} placeholder="Confirm new PIN" />
</div>
{#if state.pinError}
<p class="text-sm text-destructive">{state.pinError}</p>
{/if}
</div>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={() => (state.setPinDialogOpen = false)}>Cancel</AlertDialog.Cancel>
<AlertDialog.Action onclick={() => state.handlePinChange()}>Confirm</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+1 -1
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import { logger } from "$lib/utils/log.svelte";
import { logger } from "$lib/services/log.svelte";
import { DEFAULT_CONFIG, DEFAULT_DEVICE_INFO, VENDORS } from "$lib/device/constants.svelte";
import type {
DeviceConfig,
+152
View File
@@ -0,0 +1,152 @@
<script lang="ts">
import { onMount } from "svelte";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { Home, Info, Maximize, Minimize, Minus, RefreshCw, ScrollText, Settings, ShieldCheck, X } from "@lucide/svelte";
import type { Component } from "svelte";
import { Button } from "$lib/components/ui/button";
import { Badge } from "$lib/components/ui/badge";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import { device } from "$lib/device/manager.svelte";
type View = "home" | "config" | "security" | "logs" | "about";
interface Props {
currentView: View;
onViewChange: (view: View) => void;
children: import("svelte").Snippet;
}
let { currentView, onViewChange, children }: Props = $props();
let isMaximized = $state(false);
let unlistenResize: () => void;
const appWindow = getCurrentWindow();
// Menu items configuration
const menuItems: Array<{ view: View; icon: Component; label: string }> = [
{ view: "home", icon: Home, label: "Home" },
{ view: "config", icon: Settings, label: "Configuration" },
{ view: "security", icon: ShieldCheck, label: "Security" },
{ view: "logs", icon: ScrollText, label: "Logs" },
{ view: "about", icon: Info, label: "About" },
];
function minimize() {
appWindow.minimize();
}
async function toggleMaximize() {
await appWindow.toggleMaximize();
}
function closeApp() {
appWindow.close();
}
onMount(() => {
const setupWindow = async () => {
isMaximized = await appWindow.isMaximized();
unlistenResize = await appWindow.onResized(async () => {
isMaximized = await appWindow.isMaximized();
});
};
setupWindow();
return () => {
if (unlistenResize) unlistenResize();
};
});
</script>
<Sidebar.Provider>
<Sidebar.Root collapsible="icon">
<Sidebar.Header>
<div class="flex items-center gap-3 p-2">
<img src="/pico-forge.svg" alt="PicoForge Logo" class="h-8 w-8 rounded-sm shadow-sm" />
<span class="font-bold text-lg tracking-tight group-data-[collapsible=icon]:hidden">PicoForge</span>
</div>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Menu</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each menuItems as item}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={currentView === item.view} onclick={() => onViewChange(item.view)}>
<svelte:component this={item.icon} />
<span>{item.label}</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer class="border-t bg-background/50 p-2 group-data-[collapsible=icon]:p-2">
<div class="p-2 space-y-3 group-data-[collapsible=icon]:hidden">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-muted-foreground">Device Status</span>
{#if device.connected}
<Badge variant="default" class="bg-green-600 hover:bg-green-600 text-[10px] px-1.5 h-5">Online</Badge>
{:else}
<Badge variant="destructive" class="text-[10px] px-1.5 h-5">Offline</Badge>
{/if}
</div>
<Button variant="outline" size="sm" class="w-full gap-2" disabled={device.loading} onclick={() => device.refresh()}>
{#if device.loading}
<RefreshCw class="h-3.5 w-3.5 animate-spin" />
{:else}
<RefreshCw class="h-3.5 w-3.5" />
{/if}
Refresh
</Button>
</div>
<div class="hidden group-data-[collapsible=icon]:flex flex-col items-center justify-center p-2 gap-2">
<Button variant="ghost" size="icon" disabled={device.loading} onclick={() => device.refresh()}>
<RefreshCw class="h-4 w-4 {device.loading ? 'animate-spin' : ''}" />
</Button>
<div class={`h-2 w-2 rounded-full ${device.connected ? "bg-green-500" : "bg-red-500"}`}></div>
</div>
</Sidebar.Footer>
</Sidebar.Root>
<Sidebar.Inset>
<header data-tauri-drag-region class="h-10 bg-background border-b flex items-center justify-between px-2 select-none sticky top-0 z-10">
<div class="flex items-center gap-2">
<Sidebar.Trigger class="h-8 w-8" />
<div class="text-xs font-medium text-muted-foreground pointer-events-none flex items-center gap-2"></div>
</div>
<div class="flex items-center gap-1">
<Button variant="ghost" size="icon" class="h-8 w-8 hover:bg-muted" onclick={minimize}>
<Minus class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" class="h-8 w-8 hover:bg-muted" onclick={toggleMaximize}>
{#if isMaximized}
<Minimize class="h-3.5 w-3.5 rotate-180" />
{:else}
<Maximize class="h-3.5 w-3.5" />
{/if}
</Button>
<Button variant="ghost" size="icon" class="h-8 w-8 hover:bg-red-500 hover:text-white transition-colors" onclick={closeApp}>
<X class="h-4 w-4" />
</Button>
</div>
</header>
<main class="flex-1 overflow-hidden h-[calc(100vh-2.5rem)]">
{@render children()}
</main>
</Sidebar.Inset>
</Sidebar.Provider>
+108
View File
@@ -0,0 +1,108 @@
import { device } from "$lib/device/manager.svelte";
class configState {
setPinDialogOpen = $state(false);
currentPin = $state("");
newPin = $state("");
confirmPin = $state("");
pinError = $state("");
isSettingPin = $state(false);
minPinDialogOpen = $state(false);
minPinCurrentPin = $state("");
minPinNewPin = $state("");
minPinConfirmPin = $state("");
minPinLength = $state(4);
minPinError = $state("");
dialogOpen = $state(false);
dialogTitle = $state("");
dialogMessage = $state("");
async handleSave() {
const result = await device.save();
if (result) {
this.showStatusDialog(result.success ? "Success" : "Write Failed", result.msg);
}
}
showStatusDialog(title: string, message: string) {
this.dialogTitle = title;
this.dialogMessage = message;
this.dialogOpen = true;
}
openPinDialog() {
this.pinError = "";
this.currentPin = "";
this.newPin = "";
this.confirmPin = "";
if (device.fidoInfo?.options?.clientPin) {
this.isSettingPin = false;
} else {
this.isSettingPin = true;
}
this.setPinDialogOpen = true;
}
async openMinPinDialog() {
this.minPinError = "";
this.minPinCurrentPin = "";
this.minPinNewPin = "";
this.minPinConfirmPin = "";
this.minPinLength = device.fidoInfo?.minPinLength || 4;
this.minPinDialogOpen = true;
}
async handlePinChange() {
this.pinError = "";
if (this.newPin.length < (device.fidoInfo?.minPinLength || 4)) {
this.pinError = `PIN must be at least ${device.fidoInfo?.minPinLength || 4} characters`;
return;
}
if (this.newPin !== this.confirmPin) {
this.pinError = "PINs do not match";
return;
}
const result = await device.changePin(this.isSettingPin ? null : this.currentPin, this.newPin);
if (result.success) {
this.setPinDialogOpen = false;
this.showStatusDialog("Success", this.isSettingPin ? "PIN set successfully" : "PIN changed successfully");
} else {
this.pinError = result.msg as string;
}
}
async handleMinPinChange() {
this.minPinError = "";
if (this.minPinLength < 4 || this.minPinLength > 63) {
this.minPinError = "Minimum PIN length must be between 4 and 63";
return;
}
if (this.minPinNewPin.length < this.minPinLength) {
this.minPinError = `New PIN must be at least ${this.minPinLength} characters`;
return;
}
if (this.minPinNewPin !== this.minPinConfirmPin) {
this.minPinError = "New PINs do not match";
return;
}
const minLengthResult = await device.updateMinPinLength(this.minPinCurrentPin, this.minPinLength);
if (!minLengthResult.success) {
this.minPinError = minLengthResult.msg as string;
return;
}
const pinResult = await device.changePin(this.minPinCurrentPin, this.minPinNewPin);
if (pinResult.success) {
this.minPinDialogOpen = false;
this.showStatusDialog(
"Success",
`Minimum PIN length updated to ${this.minPinLength} and PIN changed successfully`,
);
} else {
this.minPinError = pinResult.msg as string;
}
}
}
export const configViewState = new configState();
+52
View File
@@ -0,0 +1,52 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import { Badge } from "$lib/components/ui/badge";
import { Github } from "@lucide/svelte";
import { open } from "@tauri-apps/plugin-shell";
async function openGithub() {
await open("https://github.com/librekeys/picoforge");
}
</script>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold tracking-tight">About</h1>
</div>
<Card.Root>
<Card.Content class="pt-6">
<div class="flex flex-col items-center justify-center space-y-4 text-center py-8">
<div class="bg-primary text-primary p-6 rounded-2xl">
<img src="/build-configure-symbolic.svg" alt="PicoForge Logo" class="h-12 w-12" />
</div>
<h2 class="text-2xl font-bold">PicoForge</h2>
<Badge variant="secondary" class="px-4 py-1">v0.1.5-Alpha</Badge>
<p class="text-muted-foreground max-w-md">
An open source commissioning tool for Pico FIDO security keys. Developed with Rust, Tauri, and Svelte.
</p>
<div class="text-sm text-muted-foreground space-y-1 pt-4 border-t w-full max-w-xs">
<div class="flex justify-between">
<span>Code By:</span> <span class="font-medium text-foreground">Suyog Tandel</span>
</div>
<div class="flex justify-between items-center pt-2 mt-2">
<span class="flex items-center gap-1">Copyright:</span>
<span class="font-medium text-foreground">© 2026 Suyog Tandel</span>
</div>
</div>
<div class="flex gap-4 pt-6">
<Button variant="outline" size="sm" class="gap-2" onclick={openGithub}>
<Github class="h-4 w-4" />
GitHub
</Button>
</div>
</div>
</Card.Content>
</Card.Root>
</div>
+251
View File
@@ -0,0 +1,251 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Switch } from "$lib/components/ui/switch";
import { Separator } from "$lib/components/ui/separator";
import { Slider } from "$lib/components/ui/slider/index.js";
import * as Card from "$lib/components/ui/card";
import * as Alert from "$lib/components/ui/alert";
import * as Select from "$lib/components/ui/select";
import { device } from "$lib/device/manager.svelte";
import { LED_DRIVERS, VENDORS } from "$lib/device/constants.svelte";
import { Microchip, RefreshCw, Save, Settings, Tag, TriangleAlert, X, Key } from "@lucide/svelte";
import { configViewState as state } from "$lib/state/configState.svelte";
</script>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold tracking-tight">Configuration</h1>
<p class="text-muted-foreground">Customize device settings and behavior.</p>
</div>
{#if !device.connected}
<Alert.Root>
<TriangleAlert class="h-4 w-4" />
<Alert.Title>No Device Connected</Alert.Title>
<Alert.Description>Connect your device to access configuration options.</Alert.Description>
</Alert.Root>
{:else}
<div class="grid gap-6 lg:grid-cols-2">
<Card.Root class="lg:col-span-2">
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Key class="h-5 w-5" />
PIN Management
</Card.Title>
<Card.Description>Configure FIDO2 PIN security</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="flex items-center justify-between p-4 border rounded-lg">
<div class="space-y-1">
<p class="font-medium">Current PIN Status</p>
<p class="text-sm text-muted-foreground">
{device.fidoInfo?.options?.clientPin ? "PIN is set" : "No PIN configured"}
</p>
</div>
<Button variant="outline" onclick={() => state.openPinDialog()}>
{device.fidoInfo?.options?.clientPin ? "Change PIN" : "Set PIN"}
</Button>
</div>
<div class="flex items-center justify-between p-4 border rounded-lg opacity-60">
<div class="space-y-1">
<p class="font-medium">Minimum PIN Length</p>
<p class="text-sm text-muted-foreground">
Current: {device.fidoInfo?.minPinLength || 4} characters
</p>
</div>
<Button variant="outline" disabled={true}>Update Minimum Length</Button>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Tag class="h-5 w-5" />
Identity
</Card.Title>
<Card.Description>USB Identification settings</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="space-y-2">
<Label>Vendor Preset</Label>
<Select.Root
type="single"
value={device.selectedVendor}
onValueChange={(v) => device.setVendor(v)}
disabled={!device.connected}
>
<Select.Trigger class="w-full">
{VENDORS.find((v) => v.value === device.selectedVendor)?.label ?? "Select a vendor"}
</Select.Trigger>
<Select.Content>
{#each VENDORS as vendor}
<Select.Item value={vendor.value} label={vendor.label}>
{vendor.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="vid">Vendor ID (HEX)</Label>
<Input
id="vid"
bind:value={device.config.vid}
maxlength={4}
placeholder="CAFE"
disabled={!device.connected || device.selectedVendor !== "custom"}
class="font-mono"
/>
</div>
<div class="space-y-2">
<Label for="pid">Product ID (HEX)</Label>
<Input
id="pid"
bind:value={device.config.pid}
maxlength={4}
placeholder="4242"
disabled={!device.connected || device.selectedVendor !== "custom"}
class="font-mono"
/>
</div>
</div>
<Separator />
<div class="space-y-2">
<Label for="product">Product Name</Label>
<Input id="product" bind:value={device.config.productName} placeholder="My Key" disabled={!device.connected} />
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Microchip class="h-5 w-5" />
LED Settings
</Card.Title>
<Card.Description>Adjust visual feedback behavior</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="space-y-2">
<Label for="led-gpio">LED GPIO Pin</Label>
<Input id="led-gpio" type="number" bind:value={device.config.ledGpio} min="0" max="29" />
</div>
<div class="space-y-2">
<Label for="led-driver">LED Driver</Label>
<Select.Root type="single" bind:value={device.config.ledDriver} disabled={!device.connected}>
<Select.Trigger class="w-full">
{LED_DRIVERS.find((d) => d.value === device.config.ledDriver)?.label ?? "Select driver"}
</Select.Trigger>
<Select.Content>
{#each LED_DRIVERS as driver}
<Select.Item value={driver.value} label={driver.label}>
{driver.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Separator />
<div class="space-y-2">
<Label for="led-brightness">Brightness (0-15)</Label>
<div class="flex items-center gap-4">
<Slider
type="single"
bind:value={device.config.ledBrightness}
max={15}
step={1}
disabled={!device.connected}
class="flex-1"
/>
<span class="text-xs text-muted-foreground min-w-[4ch]">Level {device.config.ledBrightness}</span>
</div>
</div>
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>LED Dimmable</Label>
<p class="text-sm text-muted-foreground">Allow brightness adjustment</p>
</div>
<Switch bind:checked={device.config.ledDimmable} />
</div>
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>LED Steady Mode</Label>
<p class="text-sm text-muted-foreground">Keep LED on constantly</p>
</div>
<Switch bind:checked={device.config.ledSteady} />
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Settings class="h-5 w-5" />
Touch & Timing
</Card.Title>
<Card.Description>Configure interaction timeouts</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="space-y-2">
<Label for="touch-timeout">Touch Timeout (seconds)</Label>
<Input id="touch-timeout" type="number" bind:value={device.config.touchTimeout} min="1" max="255" />
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Settings class="h-5 w-5" />
Device Options
</Card.Title>
<Card.Description>Toggle advanced features</Card.Description>
</Card.Header>
<Card.Content class="space-y-4">
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>Power Cycle on Reset</Label>
<p class="text-sm text-muted-foreground">Restart device on reset</p>
</div>
<Switch bind:checked={device.config.powerCycleOnReset} />
</div>
<div class="flex items-center justify-between space-x-2">
<div class="space-y-0.5">
<Label>Enable Secp256k1</Label>
<p class="text-sm text-muted-foreground">Does not work on Android!</p>
</div>
<Switch bind:checked={device.config.enableSecp256k1} />
</div>
</Card.Content>
</Card.Root>
</div>
<div class="flex justify-end">
<Button onclick={() => state.handleSave()} disabled={device.loading}>
{#if device.loading}
<RefreshCw class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Save class="mr-2 h-4 w-4" />
{/if}
Apply Changes
</Button>
</div>
{/if}
</div>
+184
View File
@@ -0,0 +1,184 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import { Badge } from "$lib/components/ui/badge";
import { Progress } from "$lib/components/ui/progress/index.js";
import * as Card from "$lib/components/ui/card";
import * as Alert from "$lib/components/ui/alert";
import { device } from "$lib/device/manager.svelte";
import { Cpu, Lock, LockOpen, Microchip, ShieldCheck, TriangleAlert, Shield } from "@lucide/svelte";
</script>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold tracking-tight">Device Overview</h1>
<p class="text-muted-foreground">Quick view of your device status and specifications.</p>
</div>
{#if !device.connected}
<Alert.Root>
<TriangleAlert class="h-4 w-4" />
<Alert.Title>No Device Connected</Alert.Title>
<Alert.Description>Please connect your device and click Refresh to begin.</Alert.Description>
</Alert.Root>
{:else}
<div class="grid gap-6 md:grid-cols-2">
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Cpu class="h-5 w-5" />
Device Information
</Card.Title>
</Card.Header>
<Card.Content class="space-y-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="space-y-1">
<p class="text-muted-foreground">Serial Number</p>
<p class="font-mono font-medium">{device.info.serial}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">Firmware Version</p>
<p class="font-mono font-medium">v{device.info.firmwareVersion}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">VID:PID</p>
<p class="font-mono font-medium">{device.config.vid}:{device.config.pid}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">Product Name</p>
<p class="font-medium truncate">{device.config.productName}</p>
</div>
</div>
<Separator />
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Flash Memory</span>
<span class="font-medium">
{device.info.flashUsed} / {device.info.flashTotal} KB
</span>
</div>
<Progress value={(device.info.flashUsed / device.info.flashTotal) * 100} class="h-2" />
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Shield class="h-5 w-5" />
FIDO2 Information
</Card.Title>
</Card.Header>
<Card.Content class="space-y-4">
{#if device.fidoInfo}
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="space-y-1">
<p class="text-muted-foreground">FIDO Version</p>
<p class="font-medium">{device.fidoInfo.versions[0] || "N/A"}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">PIN Set</p>
<p class="font-medium">
{device.fidoInfo.options?.clientPin ? "Yes" : "No"}
</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">Min PIN Length</p>
<p class="font-medium">{device.fidoInfo.minPinLength}</p>
</div>
<div class="space-y-1">
<p class="text-muted-foreground">Resident Keys</p>
<p class="font-medium">
{device.fidoInfo.options?.rk ? "Supported" : "Not Supported"}
</p>
</div>
</div>
<Separator />
<div class="space-y-1">
<p class="text-muted-foreground text-sm">AAGUID</p>
<p class="font-mono text-xs break-all">{device.fidoInfo.aaguid}</p>
</div>
{:else}
<p class="text-muted-foreground text-sm">FIDO information not available</p>
{/if}
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<Microchip class="h-5 w-5" />
LED Configuration
</Card.Title>
</Card.Header>
<Card.Content class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">LED GPIO Pin</span>
<span class="font-medium">GPIO {device.config.ledGpio}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">LED Brightness</span>
<span class="font-medium">{device.config.ledBrightness}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Presence Touch Timeout</span>
<span class="font-medium">{device.config.touchTimeout}s</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">LED Dimmable</span>
<Badge variant={device.config.ledDimmable ? "default" : "secondary"}>
{device.config.ledDimmable ? "Yes" : "No"}
</Badge>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">LED Steady Mode</span>
<Badge variant={device.config.ledSteady ? "default" : "secondary"}>
{device.config.ledSteady ? "On" : "Off"}
</Badge>
</div>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<ShieldCheck class="h-5 w-5" />
Security Status
</Card.Title>
</Card.Header>
<Card.Content class="space-y-3 text-sm">
<div class="flex justify-between items-center">
<span class="text-muted-foreground">Boot Mode</span>
<div class="flex items-center gap-2">
{#if device.security.secureBoot}
<Lock class="h-3 w-3 text-green-500" />
{:else}
<LockOpen class="h-3 w-3 text-amber-500" />
{/if}
<Badge variant={device.security.secureBoot ? "default" : "secondary"}>
{device.security.secureBoot ? "Secure Boot" : "Development"}
</Badge>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-muted-foreground">Debug Interface</span>
<span class="font-medium">
{device.security.secureLock ? "Read-out Locked" : "Debug Enabled"}
</span>
</div>
<div class="flex justify-between items-center">
<span class="text-muted-foreground">Secure Lock</span>
<Badge variant={device.security.confirmed ? "destructive" : "outline"}>
{device.security.confirmed ? "Acknowledged" : "Pending"}
</Badge>
</div>
</Card.Content>
</Card.Root>
</div>
{/if}
</div>
+55
View File
@@ -0,0 +1,55 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import { ScrollArea } from "$lib/components/ui/scroll-area";
import { logger } from "$lib/services/log.svelte";
import { Terminal } from "@lucide/svelte";
</script>
<div class="space-y-6 h-full flex flex-col">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">System Logs</h1>
<p class="text-muted-foreground">Real-time device communication and application events.</p>
</div>
<Button variant="outline" size="sm" onclick={() => (logger.logs = [])}>Clear Logs</Button>
</div>
<Card.Root class="flex-1 flex flex-col min-h-[500px] bg-black border-zinc-800">
<Card.Content class="p-0 flex-1 flex flex-col">
{#if logger.logs.length === 0}
<div class="flex-1 flex flex-col items-center justify-center text-zinc-500">
<Terminal class="h-12 w-12 mb-4 opacity-50" />
<p>No events recorded yet.</p>
</div>
{:else}
<ScrollArea class="h-[500px] p-4 font-mono text-sm" orientation="vertical">
<div class="space-y-1">
{#each logger.logs as log}
<div class="flex gap-3">
<span class="text-zinc-500 shrink-0">[{log.timestamp}]</span>
<span
class={`break-all ${
log.type === "error"
? "text-red-400"
: log.type === "success"
? "text-green-400"
: log.type === "warning"
? "text-yellow-400"
: "text-zinc-300"
}`}
>
{#if log.type === "success"}{/if}
{#if log.type === "error"}{/if}
{log.message}
</span>
</div>
{/each}
</div>
</ScrollArea>
{/if}
</Card.Content>
</Card.Root>
</div>
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Label } from "$lib/components/ui/label";
import { Switch } from "$lib/components/ui/switch";
import { Separator } from "$lib/components/ui/separator";
import * as Card from "$lib/components/ui/card";
import * as Alert from "$lib/components/ui/alert";
import { device } from "$lib/device/manager.svelte";
import { logger } from "$lib/services/log.svelte";
import { Lock, TriangleAlert } from "@lucide/svelte";
async function lockDevice() {
logger.add("Action Blocked: Secure Boot toggle attempted but feature is disabled.", "warning");
return;
}
</script>
<div class="space-y-6">
<div>
<h1 class="text-3xl font-bold tracking-tight text-destructive">Secure Boot</h1>
<p class="text-muted-foreground">Permanently lock this device to the current firmware vendor.</p>
</div>
<Alert.Root variant="destructive">
<TriangleAlert class="h-4 w-4" />
<Alert.Title>Feature Unstable</Alert.Title>
<Alert.Description>This feature is currently under work and disabled for safety.</Alert.Description>
</Alert.Root>
<Card.Root class="border-destructive/30 opacity-60">
<Card.Header>
<Card.Title>Lock Settings</Card.Title>
</Card.Header>
<Card.Content class="space-y-6 pointer-events-none">
<div class="flex items-center justify-between space-x-2">
<div class="flex flex-col space-y-1">
<Label for="secure-boot">Enable Secure Boot</Label>
<p class="font-normal text-xs text-muted-foreground">Verifies firmware signature on startup</p>
</div>
<Switch
id="secure-boot"
bind:checked={device.security.secureBoot}
disabled={true}
title="Status only - cannot be toggled via software"
/>
</div>
<div class="flex items-center justify-between space-x-2">
<div class="flex flex-col space-y-1">
<Label for="secure-lock">Secure Lock</Label>
<p class="font-normal text-xs text-muted-foreground">Prevents reading key material via debug ports</p>
</div>
<Switch id="secure-lock" bind:checked={device.security.secureLock} disabled={true} />
</div>
<Separator />
<div class="flex items-center space-x-2 bg-destructive/10 p-4 rounded-md border border-destructive/20">
<Switch id="confirm" bind:checked={device.security.confirmed} disabled={true} />
<Label for="confirm" class="text-destructive font-medium">I understand the risks of bricking my device.</Label>
</div>
</Card.Content>
<Card.Footer class="border-t bg-muted/20 px-6 py-4 flex justify-end">
<Button variant="destructive" disabled={true} onclick={lockDevice}>
<Lock class="mr-2 h-4 w-4" />
Permanently Lock Device
</Button>
</Card.Footer>
</Card.Root>
</div>
+36 -947
View File
File diff suppressed because it is too large Load Diff