Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="Network scanning hub for remote document scanning" />
<meta name="theme-color" content="#38bdf8" />
<meta name="theme-color" content="#007aff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Scan2Target" />
Expand Down
2 changes: 1 addition & 1 deletion app/web/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"start_url": "/",
"display": "standalone",
"background_color": "#0f1624",
"theme_color": "#6ae3ff",
"theme_color": "#007aff",
"orientation": "any",
"scope": "/",
"icons": [
Expand Down
10 changes: 7 additions & 3 deletions app/web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
import Toast from './components/ui/Toast.svelte';
import LoginOverlay from './components/LoginOverlay.svelte';
import NewScanView from './views/NewScanView.svelte';
import DashboardView from './views/DashboardView.svelte';
import HistoryView from './views/HistoryView.svelte';
import ManageView from './views/ManageView.svelte';
import SettingsView from './views/SettingsView.svelte';

let state;
const unsubscribe = appStore.subscribe((value) => state = value);
$: labels = $lang === 'de' ? {
scan: ['Scannen', 'Ein klarer Weg vom Papier zur fertigen Datei.'], history: ['Verlauf', 'Letzte Scans und fehlgeschlagene Zustellungen.'],
scan: ['Scannen', 'Ein klarer Weg vom Papier zur fertigen Datei.'], overview: ['Übersicht', 'Status, Scanner und letzte Aktivität auf einen Blick.'],
history: ['Verlauf', 'Letzte Scans und fehlgeschlagene Zustellungen.'],
manage: ['Verwalten', 'Scanner, Ziele und Profile konfigurieren.'], settings: ['Einstellungen', 'Oberfläche, Anmeldung und Integrationen.']
} : {
scan: ['Scan', 'A clear path from paper to the finished file.'], history: ['History', 'Recent scans and failed deliveries.'],
scan: ['Scan', 'A clear path from paper to the finished file.'], overview: ['Overview', 'Status, scanners and recent activity at a glance.'],
history: ['History', 'Recent scans and failed deliveries.'],
manage: ['Manage', 'Configure scanners, targets and profiles.'], settings: ['Settings', 'Interface, sign-in and integrations.']
};
$: pageTitle = labels[state?.page] || labels.scan;
Expand All @@ -34,12 +37,13 @@
});
</script>

<div class="app-shell" data-theme={state.settings.theme}>
<div class="app-shell" data-theme={state.settings.theme} data-density={state.settings.compactTables ? 'compact' : 'comfortable'}>
<Sidebar {pages} current={state.page} wsConnected={state.wsConnected} onNavigate={appStore.setPage} />
<main class="main-area">
<Topbar title={pageTitle[0]} subtitle={pageTitle[1]} loading={state.loading} {activeCount} onRefresh={appStore.refreshAll} onShowActive={() => appStore.setPage('history')} />
<div class="view-container">
{#if state.page === 'scan'}<NewScanView data={state} onNotify={appStore.notify} onNavigate={appStore.setPage} />
{:else if state.page === 'overview'}<DashboardView data={state} onNotify={appStore.notify} onNavigate={appStore.setPage} />
{:else if state.page === 'history'}<HistoryView data={state} onHistory={appStore.replaceHistory} onNotify={appStore.notify} />
{:else if state.page === 'manage'}<ManageView data={state} onDevices={appStore.replaceDevices} onTargets={appStore.replaceTargets} onNotify={appStore.notify} onProfilesChanged={appStore.loadCore} />
{:else}<SettingsView settings={state.settings} version={state.version} lastUpdated={state.lastUpdated} profiles={state.profiles} authConfig={state.authConfig} onChange={appStore.setSettings} onNotify={appStore.notify} onAuthChanged={appStore.loadAuthConfig} />{/if}
Expand Down
475 changes: 461 additions & 14 deletions app/web/src/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/web/src/components/layout/BottomNav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import { lang } from '../../lib/i18n';
export let current = 'scan';
export let onNavigate = () => {};
$: items = $lang === 'de' ? [['scan','scan','Scan'],['history','history','Verlauf'],['manage','devices','Verwalten'],['settings','settings','Einstellungen']] : [['scan','scan','Scan'],['history','history','History'],['manage','devices','Manage'],['settings','settings','Settings']];
$: items = $lang === 'de' ? [['scan','scan','Scan'],['overview','dashboard','Übersicht'],['history','history','Verlauf'],['manage','devices','Verwalten'],['settings','settings','Einstellungen']] : [['scan','scan','Scan'],['overview','dashboard','Overview'],['history','history','History'],['manage','devices','Manage'],['settings','settings','Settings']];
</script>
<nav class="bottom-nav" aria-label="Mobile navigation">{#each items as item}<button class:active={current === item[0]} on:click={() => onNavigate(item[0])}><Icon name={item[1]} size={20} /><span>{item[2]}</span></button>{/each}</nav>
2 changes: 1 addition & 1 deletion app/web/src/components/layout/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
export let current = 'scan';
export let wsConnected = false;
export let onNavigate = () => {};
$: labels = $lang === 'de' ? { scan: 'Scannen', history: 'Verlauf', manage: 'Verwalten', settings: 'Einstellungen', ready: 'Bereit', offline: 'Offline' } : { scan: 'Scan', history: 'History', manage: 'Manage', settings: 'Settings', ready: 'Ready', offline: 'Offline' };
$: labels = $lang === 'de' ? { scan: 'Scannen', overview: 'Übersicht', history: 'Verlauf', manage: 'Verwalten', settings: 'Einstellungen', ready: 'Bereit', offline: 'Offline' } : { scan: 'Scan', overview: 'Overview', history: 'History', manage: 'Manage', settings: 'Settings', ready: 'Ready', offline: 'Offline' };
</script>
<aside class="sidebar">
<button class="brand" on:click={() => onNavigate('scan')}><span class="logo-mark">S2</span><span><strong>Scan2Target</strong><small>Scan appliance</small></span></button>
Expand Down
1 change: 1 addition & 0 deletions app/web/src/stores/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isActive } from '../lib/status';

export const pages = [
{ id: 'scan', icon: 'scan' },
{ id: 'overview', icon: 'dashboard' },
{ id: 'history', icon: 'history' },
{ id: 'manage', icon: 'devices' },
{ id: 'settings', icon: 'settings' }
Expand Down
12 changes: 6 additions & 6 deletions app/web/src/views/DashboardView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@
<p class="hero-copy">{$t('heroCopy')}</p>
</div>
<div class="hero-actions">
<button class="btn primary" on:click={() => onNavigate('new-scan')}>
<button class="btn primary" on:click={() => onNavigate('scan')}>
<Icon name="scan" size={16} /> {$t('startScanNow')}
</button>
<button class="btn ghost" on:click={() => onNavigate('devices')}>
<button class="btn ghost" on:click={() => onNavigate('manage')}>
<Icon name="devices" size={16} /> {$t('goDevices')}
</button>
<button class="btn ghost" on:click={() => onNavigate('targets')}>
<button class="btn ghost" on:click={() => onNavigate('manage')}>
<Icon name="targets" size={16} /> {$t('goTargets')}
</button>
</div>
Expand Down Expand Up @@ -111,7 +111,7 @@
<Icon name="devices" size={28} />
<strong>{$t('noDevicesYet')}</strong>
<p class="muted small">{$t('noDevicesYetHint')}</p>
<button class="btn primary" on:click={() => onNavigate('devices')}>{$t('goDevices')}</button>
<button class="btn primary" on:click={() => onNavigate('manage')}>{$t('goDevices')}</button>
</div>
{:else}
<ul class="clean-list">
Expand All @@ -137,7 +137,7 @@
<Icon name="history" size={28} />
<strong>{$t('noHistoryYet')}</strong>
<p class="muted small">{$t('noHistoryYetHint')}</p>
<button class="btn primary" on:click={() => onNavigate('new-scan')}>{$t('startScanNow')}</button>
<button class="btn primary" on:click={() => onNavigate('scan')}>{$t('startScanNow')}</button>
</div>
{:else}
<ul class="clean-list">
Expand All @@ -161,7 +161,7 @@
</li>
{/each}
</ul>
<button class="btn ghost top-gap" on:click={() => onNavigate('history')}>{$t('history')}</button>
<button class="btn ghost top-gap" on:click={() => onNavigate('history')}>{$t('history')}</button>
{/if}
</Card>
</section>
3 changes: 2 additions & 1 deletion app/web/src/views/SettingsView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Icon from '../components/ui/Icon.svelte';
import { api, setToken } from '../lib/api';
import { appStore } from '../stores/app';
import { lang } from '../lib/i18n';
import { lang, t } from '../lib/i18n';
export let settings;
export let version = '';
export let profiles = [];
Expand Down Expand Up @@ -53,6 +53,7 @@
<section class="settings-card"><div class="settings-heading"><span class="settings-icon"><Icon name="settings" /></span><div><h2>{c.interface}</h2><p>{c.interfaceLead}</p></div></div>
<div class="setting-row"><span>{c.language}</span><div class="choice-pills compact"><button class:active={$lang === 'de'} on:click={() => lang.set('de')}>Deutsch</button><button class:active={$lang === 'en'} on:click={() => lang.set('en')}>English</button></div></div>
<div class="setting-row"><span>{c.theme}</span><select value={settings.theme} on:change={(event) => onChange({ theme: event.currentTarget.value })}><option value="system">{c.system}</option><option value="light">{c.light}</option><option value="dark">{c.dark}</option></select></div>
<label class="switch-row"><span>{$t('compactTablesLabel')}</span><input type="checkbox" checked={settings.compactTables} on:change={(event) => onChange({ compactTables: event.currentTarget.checked })} /></label>
</section>

<section class="settings-card"><div class="settings-heading"><span class="settings-icon"><Icon name="logout" /></span><div><h2>{c.account}</h2><p>{c.accountLead}</p></div></div>
Expand Down
177 changes: 113 additions & 64 deletions docs/webui-redesign-plan.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,113 @@
# WebUI Redesign Plan (2026)

## 1) Ist-Analyse

### Technologie-Stack
- Frontend: Svelte 4 + Vite 5 (`app/web`).
- Styling: globales CSS + viele Inline-Styles in `App.svelte`.
- Backend: FastAPI mit Routern unter `/api/v1/*`.
- Realtime: WebSocket über `/api/v1/ws`, zusätzlich Polling als Fallback.

### Aktueller Frontend-Aufbau
- Haupt-UI: `app/web/src/App.svelte` (monolithische Komponente).
- Mobile Quick-UI: `app/web/src/Mobile.svelte` (separate Entry-Page `/mobile`).
- Basiskomponenten: `NavBar.svelte`, `SectionCard.svelte`, `StatGrid.svelte`.

### Relevante API-Endpunkte (Frontend-Nutzung)
- Version: `GET /api/v1/version`
- Scanner/Geräte: `GET /api/v1/devices`, `GET /api/v1/devices/discover`, `POST /api/v1/devices/add`, `DELETE /api/v1/devices/{id}`
- Scans: `GET /api/v1/scan/profiles`, `POST /api/v1/scan/start`, `POST /api/v1/scan/preview`, `POST /api/v1/scan/batch`, `POST /api/v1/scan/jobs/{id}/cancel`
- Targets: `GET /api/v1/targets`, `POST /api/v1/targets`, `PUT /api/v1/targets/{id}`, `DELETE /api/v1/targets/{id}`, `POST /api/v1/targets/{id}/test`
- Historie/Status: `GET /api/v1/history`, `DELETE /api/v1/history`, `DELETE /api/v1/history/{id}`, `POST /api/v1/history/{id}/retry-upload`
- Statistik: `/api/v1/stats/*`
- Realtime: `ws://.../api/v1/ws`

### Hauptprobleme
1. **Wartbarkeit:** `App.svelte` enthält State, API-Aufrufe, Business-Logik und große Template-Blöcke.
2. **UX-Konsistenz:** viele Block-`alert()`-Meldungen statt einheitlicher UI-Feedback-Muster.
3. **Layout-Konsistenz:** viele Inline-Styles erschweren Reuse und visuelle Einheitlichkeit.
4. **Responsive Tiefe:** einige große Tabellen/Formen sind auf kleinen Breakpoints nur begrenzt optimiert.

## 2) Zielbild

- Modernes, klares Dashboard-Design (Dark-first).
- Einheitliche Karten, Typografie, Formulare, Buttons, Badges und Zustandsfarben.
- Verbesserte visuelle Hierarchie und Lesbarkeit.
- Bestehende API-Anbindung bleibt unverändert.
- Keine Feature-Regression (Scan, Batch, Targets, History, Stats, WebSocket).

## 3) Umsetzungsplan

### Phase A – Design-Fundament
- Überarbeitung globaler Tokens (Farben, Abstände, Radius, Schatten, Fokus-Ringe).
- Vereinheitlichung der Top-Navigation und Card-Komponenten.
- Bessere responsive Regeln für <= 1100px, <= 760px.

### Phase B – UI-Konsolidierung
- Reduktion von Inline-Styling zugunsten zentraler Klassen.
- Bessere Darstellungsregeln für Listen, Panels, Tabellen und Formularfelder.
- Einheitliches visuelles Verhalten für Hover/Focus/Disabled.

### Phase C – Feature-Sicherheit
- Bestehende API- und WebSocket-Flows unverändert beibehalten.
- Build prüfen (`npm run build`) und visuelle Regression minimal halten.

## 4) Geplante Dateianpassungen
- `app/web/src/app.css` (komplettes visuelles Redesign-Fundament)
- `app/web/src/components/NavBar.svelte` (modernisierte Header-Navigation)
- `app/web/src/components/SectionCard.svelte` (saubere Card-Header-Struktur)
- `app/web/src/components/StatGrid.svelte` (verbesserte KPI-Karten)

## 5) Nicht-Ziele in diesem Schritt
- Keine API-Änderungen im Backend.
- Keine Entfernung bestehender Features.
- Kein schweres neues UI-Framework.
# WebUI Design-System (Redesign 2026)

Referenz für die Oberfläche in `app/web`. Das Dokument löst den alten
Redesign-Plan ab: die dort beschriebene monolithische `App.svelte` mit
`alert()`-Meldungen und separater `Mobile.svelte` existiert nicht mehr.

## 1. Scope

Visuelles/UX-Redesign auf der bestehenden Svelte-4-Basis. Architektur,
REST-API, WebSockets, i18n (de/en), PWA und Dark-Mode bleiben unangetastet —
kein Framework-Rebuild, keine neuen Runtime-Dependencies.

## 2. Verbindliche Design-Entscheidungen

| Achse | Entscheidung |
|---|---|
| Tiefe | Visuelles/UX-Redesign auf Svelte-4-Basis. Kein Rewrite. |
| Visuelle Sprache | **iOS-Soft** — große Radien, weiche Schatten, luftig, freundlich. |
| Referenz-Modus | **Light = Held**, Dark **gleichwertig** über *Elevation* (Flächen-Ebenen) statt Schatten. Default bleibt `theme: 'system'`. |
| Akzent/Marke | **iOS-Blau** `#007aff` (Dark `#0a84ff`). Logo, PWA `theme-color` und UI sind darauf vereinheitlicht. |
| Navigation | 5 Items: **Scan · Übersicht · Verlauf · Verwalten · Einstellungen**. `Verwalten` bleibt konsolidiert (Devices/Targets/Profiles als Tabs *innerhalb* der View). |
| Startseite | **Scan** (schnellster Weg zur Kernaktion). |
| Styling | **Vanilla CSS** mit Token-Layer. Kein Tailwind, keine Component-Lib. |
| Icons | Inline-SVG-Set in `components/ui/Icon.svelte`, bei Bedarf mit Lucide-Pfaden erweitern. Dep-frei bleiben. |
| Typo | **Inter** (via `@fontsource/inter` gebündelt). |
| Dichte | Komfortabel als Default, `compactTables` schaltet auf `[data-density="compact"]`. |
| Listen | iOS-„inset grouped" (eine gerundete Fläche mit Hairline-Trennern), nicht N schwebende Einzelkärtchen. |

**Anti-Muster (bewusst vermieden):** keine ALL-CAPS-Eyebrows, kein „→" an
Button-/Link-Text, kein Monospace für UI-Labels, nicht „ein Radius/ein
Schatten für alles". Radien sind gestaffelt (Hero 24 / Karte 20 / Control 12).

## 3. Tokens

Source of Truth ist `app/web/src/app.css`, Sektion 1. Neue Screens **nur** mit
diesen Variablen bauen, keine Hex-Werte hart reinschreiben.

**Light**
```
--bg:#eef1f6 --surface:#fff --surface-2:#f5f7fb --surface-3:#eef2f8
--text:#1c2530 --muted:#6b7688 --faint:#98a2b3 --line:#e4e9f1 --line-strong:#d5dce8
--accent:#007aff --accent-strong:#0062cc --accent-soft:#e8f1ff --on-accent:#fff
--success:#1f9d55 --warn:#c77700 --danger:#d63a2f --info:#2f6df0 (+ *-soft)
--r-lg:24 --r-card:20 --r-ctl:12 --r-sm:10 --r-pill:999
--sh-sm/-md/-lg (Light: echte Schatten)
```

**Dark** (`[data-theme="dark"]` + `@media(prefers-color-scheme:dark) [data-theme="system"]`)
```
--bg:#0f1420 --surface:#171d2b --surface-2:#1f2637 --surface-3:#252d40
--accent:#0a84ff … --sh-sm/-md/-lg: none
```

**Dark-Shadows sind bewusst `none`.** Elevation in Dark kommt aus einer höheren
`--surface-*`-Stufe plus Border — keine Schatten faken.

## 4. Aufbau von `app.css`

20 kommentierte Sektionen: Tokens → Reset → Shell → Topbar → Buttons → Forms →
Cards → Badges → Inset-Listen → Overview (Hero/KPIs) → Scan-Flow → History →
Manage/Resource → Settings → Statistics → Dialogs → BottomNav → Motion →
Responsive → Density.

Klassennamen sind bewusst stabil gehalten; die Views erben den Look über die
gemeinsamen Klassen (`.btn.primary/.secondary/.ghost/.danger`, `.card`,
`.badge.<tone>`, `.clean-list/.list-row`, `.resource-card`, `.settings-card`).

## 5. Dichte / Compact-Modus

`settings.compactTables` (localStorage, Default `false`) setzt in `App.svelte`
`data-density="compact"` auf `.app-shell` — analog zu `data-theme`. Sektion 20
in `app.css` reduziert daraufhin Padding und Zeilenhöhe in Listen, History-,
Resource- und Settings-Karten. Der Schalter liegt in **Einstellungen →
Oberfläche**.

## 6. Leitplanken für Folgearbeit

- Tokens und bestehende Klassen wiederverwenden. Konsistenz vor Kreativität.
- Keine neuen Dependencies. Icons via `Icon.svelte` + Lucide-Pfade.
- API-, i18n- und WebSocket-Flows nicht anfassen; Feature-Parität wahren
(Scan, Batch, Targets, History, Stats).
- Dark-Mode bei jeder neuen UI mitdenken (Elevation statt Schatten).

## 7. Verifikation

```bash
cd app/web
npm ci
npm run build # muss durchlaufen
npm run check # 0 errors / 0 warnings
npm run test:e2e # Playwright-Durchlauf des Multipage-Scans
```

Regressions-Check nach Änderungen an `app.css` (jede genutzte Klasse muss
abgedeckt sein):

```bash
comm -23 \
<(grep -rhoE 'class="[^"]*"' app/web/src --include=*.svelte | sed 's/class="//;s/"//' | tr ' ' '\n' | sed 's/{.*}//' | grep -vE '^\s*$' | sort -u) \
<(grep -oE '\.[a-zA-Z][a-zA-Z0-9_-]+' app/web/src/app.css | sed 's/^\.//' | sort -u)
```

Erwartung: keine Treffer.

## 8. Offene Punkte

- `views/StatisticsView.svelte` ist derzeit in keiner Route eingebunden; die
Chart-Styles (Sektion 15) sind vorhanden, die View aber nicht erreichbar.
- Für die Status `delivery_failed`, `scanning`, `processing`, `delivering` und
`retry_scheduled` fehlen `status_*`-Keys in `lib/i18n.js`; Badges zeigen dort
den Rohwert.
- Theme-/Sprach-Umschalter in der Topbar wäre möglich, ist aber nicht
beschlossen — aktuell liegt beides in den Einstellungen.
Loading