Files
tessera-ctl/apps/desktop/src/setup.html
T
schalli 2d55f07729 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>
2026-09-16 16:41:21 +02:00

309 lines
8.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tessera – Desktop-App einrichten</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: oklch(0.17 0.01 260);
color: oklch(0.95 0 0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.setup-card {
text-align: center;
max-width: 440px;
width: 100%;
padding: 48px 32px;
background: oklch(0.22 0.01 260);
border-radius: 12px;
border: 1px solid oklch(0.30 0.01 260);
}
.brand-mark {
margin: 0 auto 16px;
width: 56px;
height: 56px;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 8px;
color: #ffed00;
}
.subtitle {
font-size: 0.875rem;
color: oklch(0.65 0 0);
margin-bottom: 32px;
}
label {
display: block;
text-align: left;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 8px;
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"] {
width: 100%;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid oklch(0.30 0.01 260);
background: oklch(0.20 0.01 260);
color: oklch(0.95 0 0);
font-size: 1rem;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
input[type="url"]:focus {
border-color: #9c9440;
}
input[type="url"]::placeholder {
color: oklch(0.45 0 0);
}
.error-message {
display: none;
text-align: left;
font-size: 0.8125rem;
color: oklch(0.55 0.2 27);
margin-top: 6px;
}
.warning-message {
display: none;
text-align: left;
font-size: 0.8125rem;
color: oklch(0.75 0.15 85);
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 {
margin-top: 24px;
width: 100%;
padding: 12px 32px;
border-radius: 8px;
border: none;
background: #ffed00;
color: #1a1a1a;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
font-family: inherit;
transition: opacity 0.2s;
}
button:hover {
opacity: 0.9;
}
button:active {
opacity: 0.8;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<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>
<p class="subtitle">Desktop-App einrichten</p>
<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
id="server-url"
type="url"
placeholder="https://tessera.example.com"
autocomplete="off"
spellcheck="false"
/>
<p id="error-msg" class="error-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>
</div>
<script type="module">
const { invoke } = window.__TAURI__.core;
const urlInput = document.getElementById('server-url');
const connectBtn = document.getElementById('connect-btn');
const errorMsg = document.getElementById('error-msg');
const warningMsg = document.getElementById('warning-msg');
const infoMsg = document.getElementById('info-msg');
/**
* Validate a server URL using the URL constructor.
* Returns { valid: boolean, warning: string | null, error: string | null }
*/
function validateUrl(input) {
const trimmed = input.trim();
if (!trimmed) {
return { valid: false, warning: null, error: 'Bitte geben Sie die Adresse Ihres Tessera-Servers ein.' };
}
let parsed;
try {
parsed = new URL(trimmed);
} catch {
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
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
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)
const isLocalhost = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
if (parsed.protocol === 'http:' && !isLocalhost) {
return {
valid: true,
warning: 'Hinweis: Diese Verbindung ist unverschlüsselt (http). Für den Produktivbetrieb empfehlen wir https.',
error: null,
};
}
return { valid: true, warning: null, error: null };
}
function showError(message) {
errorMsg.textContent = message;
errorMsg.style.display = 'block';
warningMsg.style.display = 'none';
infoMsg.style.display = 'none';
}
function showWarning(message) {
warningMsg.textContent = message;
warningMsg.style.display = 'block';
errorMsg.style.display = 'none';
infoMsg.style.display = 'none';
}
function showInfo(message) {
infoMsg.textContent = message;
infoMsg.style.display = 'block';
errorMsg.style.display = 'none';
}
function clearMessages() {
errorMsg.style.display = 'none';
warningMsg.style.display = 'none';
infoMsg.style.display = 'none';
}
async function connect() {
clearMessages();
const rawUrl = urlInput.value;
const result = validateUrl(rawUrl);
if (!result.valid) {
showError(result.error);
return;
}
if (result.warning) {
showWarning(result.warning);
}
// Normalize the URL
const normalizedUrl = new URL(rawUrl.trim()).href;
// Disable button while the address is being checked
connectBtn.disabled = true;
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 {
await invoke('save_server_url', { url: normalizedUrl });
} catch (err) {
showError('Die Adresse konnte nicht gespeichert werden: ' + String(err));
connectBtn.disabled = false;
connectBtn.textContent = 'Verbinden';
}
}
// Button click handler
connectBtn.addEventListener('click', connect);
// Enter key support
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
connect();
}
});
// Clear error messages on input change
urlInput.addEventListener('input', clearMessages);
</script>
</body>
</html>