Files
tessera-ctl/apps/desktop/src/setup.html
T
schalli 73ac08af17 fix(quick-260921-bi2): a11y - Bildmarke und Desktop-Einrichtungsseite
apps/web/src/app/icon.svg (Next.js-Favicon, keine next-intl-Anbindung
moeglich): <title>Tessera</title> als einzige Fundstelle, die die ganze
Bedeutung allein traegt.

apps/desktop/src/setup.html: Bildmarke steht direkt vor der Ueberschrift
"Tessera" und wird dekorativ (aria-hidden="true").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 09:21:49 +02:00

395 lines
11 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;
}
.client-info {
margin: 20px 0 0;
font-size: 0.75rem;
color: oklch(0.6 0 0);
min-height: 1em;
}
.current-server {
display: none;
text-align: left;
font-size: 0.8125rem;
color: oklch(0.75 0 0);
margin: -20px 0 24px;
}
button.secondary {
margin-top: 12px;
background: transparent;
color: oklch(0.85 0 0);
border: 1px solid oklch(0.30 0.01 260);
}
button[hidden] {
display: none;
}
</style>
</head>
<body>
<div class="setup-card">
<svg class="brand-mark" aria-hidden="true" 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" id="subtitle">Desktop-App einrichten</p>
<p id="current-server" class="current-server"></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>
<button id="cancel-btn" type="button" class="secondary" hidden>Abbrechen</button>
<p id="client-info" class="client-info"></p>
</div>
<script type="module">
const { invoke } = window.__TAURI__.core;
const urlInput = document.getElementById('server-url');
const connectBtn = document.getElementById('connect-btn');
const cancelBtn = document.getElementById('cancel-btn');
const errorMsg = document.getElementById('error-msg');
const warningMsg = document.getElementById('warning-msg');
const infoMsg = document.getElementById('info-msg');
const subtitle = document.getElementById('subtitle');
const currentServer = document.getElementById('current-server');
/**
* 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);
/**
* Aenderungsmodus: die Adresse steht bereits im Store (get_server_url
* liefert einen Wert). Feld vorbelegen, aktuelle Adresse anzeigen und
* den "Abbrechen"-Knopf einblenden. Der eingesetzte Wert kommt per
* textContent/value in die Seite, nie als HTML (T-JN2-05).
*/
function enterChangeMode(current) {
urlInput.value = current;
currentServer.textContent = `Aktuell verbunden mit: ${current}`;
currentServer.style.display = 'block';
subtitle.textContent = 'Server-Adresse ändern';
cancelBtn.hidden = false;
urlInput.focus();
urlInput.select();
}
cancelBtn.addEventListener('click', async () => {
clearMessages();
cancelBtn.disabled = true;
try {
await invoke('open_server');
} catch (err) {
showError(String(err));
cancelBtn.disabled = false;
}
});
// Beim Laden pruefen, ob bereits eine Server-Adresse gespeichert ist --
// dann ist dies der Aenderungsmodus, nicht der Erststart. Ohne
// gespeicherte Adresse (oder wenn der Aufruf fehlschlaegt) bleibt es
// beim bisherigen Erststart-Verhalten.
async function init() {
// Version und Stand der installierten App -- rein informativ; ein
// Fehlschlag laesst die Zeile einfach leer.
try {
const info = await invoke('get_client_info');
if (typeof info === 'string') {
document.getElementById('client-info').textContent = info;
}
} catch {
// Zeile bleibt leer.
}
try {
const current = await invoke('get_server_url');
if (typeof current === 'string' && current) {
enterChangeMode(current);
}
} catch {
// Erststart-Verhalten: Feld bleibt leer, kein Aenderungsmodus.
}
}
init();
</script>
</body>
</html>