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>
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 33 KiB |
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||