feat(18-04): Task 2 — Erststart-Seite in Tessera-Gestalt, echtes App-Icon, lokaler AppImage-Beweis

- setup.html spricht nur noch über window.__TAURI__.core.invoke (kein
  Modul-Import mehr, der im gebauten Client ohnehin scheiterte), prüft die
  Adresse über check_server und speichert sie über save_server_url
- Sie-Form-Texte, Tessera-Zeichen als inline-SVG, Markengelb/-Olive,
  keine vorbelegte Server-Adresse
- CSP ohne Fremdhost (unpkg.com entfernt), da nichts mehr von außen geladen wird
- Icon-Satz aus apps/web/src/app/icon.svg neu erzeugt (512, 128, 128@2x, 32,
  ICO mit 16-256) statt der bisherigen flachen gelben Quadrate
- Versehentlich verschachteltes apps/desktop/src-tauri/apps/ entfernt
- Lokaler AppImage-Bau (Tessera-1.1.0.AppImage, ~103 MB) erfolgreich, von
  desktop-collect.sh eingesammelt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 16:41:21 +02:00
parent 8b130fddbd
commit 2d55f07729
11 changed files with 80 additions and 23 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 B

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 33 KiB

+4 -1
View File
@@ -21,7 +21,7 @@
} }
], ],
"security": { "security": {
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com" "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
} }
}, },
"bundle": { "bundle": {
@@ -31,6 +31,9 @@
"nsis" "nsis"
], ],
"icon": [ "icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.png", "icons/icon.png",
"icons/icon.ico" "icons/icon.ico"
] ]
+76 -22
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tessera - Setup</title> <title>Tessera – Desktop-App einrichten</title>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -28,16 +28,22 @@
max-width: 440px; max-width: 440px;
width: 100%; width: 100%;
padding: 48px 32px; padding: 48px 32px;
background: oklch(0.23 0.01 260); background: oklch(0.22 0.01 260);
border-radius: 12px; border-radius: 12px;
border: 1px solid oklch(0.30 0.01 260); border: 1px solid oklch(0.30 0.01 260);
} }
.brand-mark {
margin: 0 auto 16px;
width: 56px;
height: 56px;
}
h1 { h1 {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
margin-bottom: 8px; margin-bottom: 8px;
color: oklch(0.91 0.19 102); color: #ffed00;
} }
.subtitle { .subtitle {
@@ -55,6 +61,13 @@
color: oklch(0.85 0 0); color: oklch(0.85 0 0);
} }
.hint {
text-align: left;
font-size: 0.8125rem;
color: oklch(0.60 0 0);
margin-bottom: 8px;
}
input[type="url"] { input[type="url"] {
width: 100%; width: 100%;
padding: 12px 16px; padding: 12px 16px;
@@ -69,7 +82,7 @@
} }
input[type="url"]:focus { input[type="url"]:focus {
border-color: oklch(0.91 0.19 102); border-color: #9c9440;
} }
input[type="url"]::placeholder { input[type="url"]::placeholder {
@@ -92,14 +105,22 @@
margin-top: 6px; margin-top: 6px;
} }
.info-message {
display: none;
text-align: left;
font-size: 0.8125rem;
color: oklch(0.70 0.15 150);
margin-top: 6px;
}
button { button {
margin-top: 24px; margin-top: 24px;
width: 100%; width: 100%;
padding: 12px 32px; padding: 12px 32px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
background: oklch(0.91 0.19 102); background: #ffed00;
color: oklch(0.20 0.02 90); color: #1a1a1a;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
@@ -123,31 +144,43 @@
</head> </head>
<body> <body>
<div class="setup-card"> <div class="setup-card">
<svg class="brand-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="56" height="56">
<rect x="4" y="4" width="64" height="64" rx="14" fill="#1a1a1a"></rect>
<g>
<rect x="16" y="16" width="12" height="12" rx="2.5" fill="#9c9440"></rect>
<rect x="30" y="16" width="12" height="12" rx="2.5" fill="#9c9440"></rect>
<rect x="45" y="15" width="12" height="12" rx="2.5" transform="rotate(12 51 21)" fill="#ffed00"></rect>
<rect x="30" y="30" width="12" height="12" rx="2.5" fill="#9c9440"></rect>
<rect x="30" y="44" width="12" height="12" rx="2.5" fill="#9c9440"></rect>
</g>
</svg>
<h1>Tessera</h1> <h1>Tessera</h1>
<p class="subtitle">Desktop-Client einrichten</p> <p class="subtitle">Desktop-App einrichten</p>
<label for="server-url">Server-URL eingeben:</label> <label for="server-url">Adresse Ihres Tessera-Servers</label>
<p class="hint">Das ist die Adresse, unter der Sie Tessera auch im Browser öffnen.</p>
<input <input
id="server-url" id="server-url"
type="url" type="url"
placeholder="https://tessera.example.com" placeholder="https://tessera.example.com"
value="http://localhost:3000"
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
/> />
<p id="error-msg" class="error-message"></p> <p id="error-msg" class="error-message"></p>
<p id="warning-msg" class="warning-message"></p> <p id="warning-msg" class="warning-message"></p>
<p id="info-msg" class="info-message"></p>
<button id="connect-btn" type="button">Verbinden</button> <button id="connect-btn" type="button">Verbinden</button>
</div> </div>
<script type="module"> <script type="module">
import { load } from '@tauri-apps/plugin-store'; const { invoke } = window.__TAURI__.core;
const urlInput = document.getElementById('server-url'); const urlInput = document.getElementById('server-url');
const connectBtn = document.getElementById('connect-btn'); const connectBtn = document.getElementById('connect-btn');
const errorMsg = document.getElementById('error-msg'); const errorMsg = document.getElementById('error-msg');
const warningMsg = document.getElementById('warning-msg'); const warningMsg = document.getElementById('warning-msg');
const infoMsg = document.getElementById('info-msg');
/** /**
* Validate a server URL using the URL constructor. * Validate a server URL using the URL constructor.
@@ -157,19 +190,23 @@
const trimmed = input.trim(); const trimmed = input.trim();
if (!trimmed) { if (!trimmed) {
return { valid: false, warning: null, error: 'Bitte eine URL eingeben.' }; return { valid: false, warning: null, error: 'Bitte geben Sie die Adresse Ihres Tessera-Servers ein.' };
} }
let parsed; let parsed;
try { try {
parsed = new URL(trimmed); parsed = new URL(trimmed);
} catch { } catch {
return { valid: false, warning: null, error: 'Ungueltige URL. Bitte eine gueltige URL eingeben (z.B. https://tessera.example.com).' }; return {
valid: false,
warning: null,
error: 'Diese Adresse ist ungültig. Bitte geben Sie eine vollständige Adresse ein, z. B. https://tessera.example.com.',
};
} }
// Only allow http and https protocols // Only allow http and https protocols
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return { valid: false, warning: null, error: 'Nur HTTP und HTTPS URLs sind erlaubt.' }; return { valid: false, warning: null, error: 'Es sind nur Adressen mit http oder https erlaubt.' };
} }
// Warn on non-https for non-localhost addresses (T-06-01 mitigation) // Warn on non-https for non-localhost addresses (T-06-01 mitigation)
@@ -177,7 +214,7 @@
if (parsed.protocol === 'http:' && !isLocalhost) { if (parsed.protocol === 'http:' && !isLocalhost) {
return { return {
valid: true, valid: true,
warning: 'Warnung: Unverschluesselte Verbindung (HTTP). Fuer Produktivumgebungen wird HTTPS empfohlen.', warning: 'Hinweis: Diese Verbindung ist unverschlüsselt (http). Für den Produktivbetrieb empfehlen wir https.',
error: null, error: null,
}; };
} }
@@ -189,17 +226,26 @@
errorMsg.textContent = message; errorMsg.textContent = message;
errorMsg.style.display = 'block'; errorMsg.style.display = 'block';
warningMsg.style.display = 'none'; warningMsg.style.display = 'none';
infoMsg.style.display = 'none';
} }
function showWarning(message) { function showWarning(message) {
warningMsg.textContent = message; warningMsg.textContent = message;
warningMsg.style.display = 'block'; warningMsg.style.display = 'block';
errorMsg.style.display = 'none'; errorMsg.style.display = 'none';
infoMsg.style.display = 'none';
}
function showInfo(message) {
infoMsg.textContent = message;
infoMsg.style.display = 'block';
errorMsg.style.display = 'none';
} }
function clearMessages() { function clearMessages() {
errorMsg.style.display = 'none'; errorMsg.style.display = 'none';
warningMsg.style.display = 'none'; warningMsg.style.display = 'none';
infoMsg.style.display = 'none';
} }
async function connect() { async function connect() {
@@ -220,18 +266,26 @@
// Normalize the URL // Normalize the URL
const normalizedUrl = new URL(rawUrl.trim()).href; const normalizedUrl = new URL(rawUrl.trim()).href;
// Disable button during save // Disable button while the address is being checked
connectBtn.disabled = true; connectBtn.disabled = true;
connectBtn.textContent = 'Verbinde...'; connectBtn.textContent = 'Prüfe Verbindung …';
let version;
try {
version = await invoke('check_server', { url: normalizedUrl });
} catch (err) {
showError(String(err));
connectBtn.disabled = false;
connectBtn.textContent = 'Verbinden';
return;
}
showInfo('Tessera ' + version + ' gefunden – Verbindung wird hergestellt …');
try { try {
const store = await load('config.json', { autoSave: true }); await invoke('save_server_url', { url: normalizedUrl });
await store.set('server_url', normalizedUrl);
// Navigate the WebView to the configured server
window.location.href = normalizedUrl;
} catch (err) { } catch (err) {
showError('Fehler beim Speichern der Konfiguration: ' + (err.message || err)); showError('Die Adresse konnte nicht gespeichert werden: ' + String(err));
connectBtn.disabled = false; connectBtn.disabled = false;
connectBtn.textContent = 'Verbinden'; connectBtn.textContent = 'Verbinden';
} }