Initial release 0.1.0
This commit is contained in:
commit
9f97d8407a
52 files changed
+6774
No files matched your search
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import NavIcon from '#lib/components/NavIcon.svelte';
|
||||
import ThemeToggle from '#lib/components/ThemeToggle.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
const navigation = [
|
||||
{ href: '/eingang', label: 'Eingang', icon: 'inbox' },
|
||||
{ href: '/fundus', label: 'Fundus', icon: 'library' },
|
||||
{ href: '/sendungen', label: 'Sendungen', icon: 'shows' },
|
||||
{ href: '/archiv', label: 'Archiv', icon: 'archive' },
|
||||
{ href: '/verwaltung', label: 'Verwaltung', icon: 'settings' }
|
||||
] as const;
|
||||
|
||||
function isActive(href: string) {
|
||||
return (
|
||||
page.url.pathname.startsWith(href) || (href === '/sendungen' && page.url.pathname === '/live')
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta
|
||||
name="description"
|
||||
content="Sendewerk organisiert redaktionelle Inhalte und heinzlive-Sendungen."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href={resolve('/fundus')} aria-label="Sendewerk Startseite">
|
||||
<span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span>Sendewerk</span>
|
||||
</a>
|
||||
|
||||
<nav class="main-navigation" aria-label="Hauptnavigation">
|
||||
{#each navigation as item (item.href)}
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
class:active={isActive(item.href)}
|
||||
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||
>
|
||||
<NavIcon name={item.icon} />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<ThemeToggle />
|
||||
<div class="stage-indicator">
|
||||
<span></span>
|
||||
<div><strong>Etappe 1</strong><small>Lokaler Entwurf</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<header class="mobile-header">
|
||||
<a class="brand" href={resolve('/fundus')} aria-label="Sendewerk Startseite">
|
||||
<span class="brand-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span>Sendewerk</span>
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
|
||||
<main id="main-content">{@render children()}</main>
|
||||
|
||||
<nav class="mobile-navigation" aria-label="Hauptnavigation">
|
||||
{#each navigation as item (item.href)}
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
class:active={isActive(item.href)}
|
||||
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||
>
|
||||
<NavIcon name={item.icon} />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
redirect(307, '/fundus');
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<svelte:head><title>Archiv | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap narrow-page">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">Bewahrte Sendungen</p>
|
||||
<h1>Archiv</h1>
|
||||
<p class="page-intro">
|
||||
Abgeschlossene Abläufe bleiben hier in ihrem damaligen Stand nachvollziehbar.
|
||||
</p>
|
||||
</header>
|
||||
<section class="quiet-panel planned-panel">
|
||||
<div class="planned-icon" aria-hidden="true">□</div>
|
||||
<p class="eyebrow">Geplant für Etappe 6</p>
|
||||
<h2>Historie braucht klare Regeln.</h2>
|
||||
<p>
|
||||
Stabile Snapshots, Verwendungshistorie und bewusste Archivierung werden gemeinsam
|
||||
implementiert, statt jetzt nur eine leere Liste vorzutäuschen.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<svelte:head><title>Eingang | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap narrow-page">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">Sammelstelle</p>
|
||||
<h1>Eingang</h1>
|
||||
<p class="page-intro">
|
||||
Neue Gedanken und spätere RSS-Fundstücke landen hier, bevor sie in den Fundus übernommen
|
||||
werden.
|
||||
</p>
|
||||
</header>
|
||||
<section class="quiet-panel planned-panel">
|
||||
<div class="planned-icon" aria-hidden="true">+</div>
|
||||
<p class="eyebrow">Geplant für Etappe 3 und 7</p>
|
||||
<h2>Noch ist der Tresen leer.</h2>
|
||||
<p>
|
||||
Schnellaufnahme, Feed-Vorschau und bewusste Übernahme werden erst mit den dazugehörigen
|
||||
Speicher- und Sicherheitsregeln umgesetzt.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getPieces } from '#lib/server/previews.js';
|
||||
|
||||
export function load() {
|
||||
return { pieces: getPieces() };
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import PreviewNotice from '#lib/components/PreviewNotice.svelte';
|
||||
import { formatClock } from '#lib/format.js';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let query = $state('');
|
||||
let filter = $state<'all' | 'ready' | 'preparing'>('all');
|
||||
let filteredPieces = $derived(
|
||||
data.pieces.filter((piece) => {
|
||||
const text =
|
||||
`${piece.title} ${piece.summary} ${piece.format} ${piece.topics.join(' ')}`.toLowerCase();
|
||||
return text.includes(query.toLowerCase()) && (filter === 'all' || piece.status === filter);
|
||||
})
|
||||
);
|
||||
|
||||
const statusLabel = {
|
||||
ready: 'Sendefertig',
|
||||
preparing: 'In Vorbereitung',
|
||||
inbox: 'Eingang',
|
||||
archived: 'Archiviert'
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Fundus | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap">
|
||||
<header class="page-heading split-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Redaktioneller Fundus</p>
|
||||
<h1>Gute Stücke, sofort griffbereit.</h1>
|
||||
<p class="page-intro">
|
||||
Vorbereitete Ideen, Gespräche und Fundstücke für die nächste Sendung.
|
||||
</p>
|
||||
</div>
|
||||
<div class="heading-stat" aria-label={`${data.pieces.length} Inhalte im Beispieldatensatz`}>
|
||||
<strong>{data.pieces.length}</strong><span>Inhalte</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PreviewNotice area="Fundus" />
|
||||
|
||||
<section class="library-toolbar" aria-label="Fundus durchsuchen und filtern">
|
||||
<label class="search-field">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<span class="sr-only">Fundus durchsuchen</span>
|
||||
<input bind:value={query} type="search" placeholder="Titel, Thema oder Format suchen" />
|
||||
</label>
|
||||
<div class="filter-tabs" aria-label="Status filtern">
|
||||
<button class:active={filter === 'all'} type="button" onclick={() => (filter = 'all')}
|
||||
>Alle</button
|
||||
>
|
||||
<button class:active={filter === 'ready'} type="button" onclick={() => (filter = 'ready')}
|
||||
>Sendefertig</button
|
||||
>
|
||||
<button
|
||||
class:active={filter === 'preparing'}
|
||||
type="button"
|
||||
onclick={() => (filter = 'preparing')}>In Arbeit</button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if filteredPieces.length > 0}
|
||||
<div class="piece-grid">
|
||||
{#each filteredPieces as piece (piece.id)}
|
||||
<article class="piece-card" style={`--category: ${piece.categoryColor}`}>
|
||||
<div class="piece-card-topline">
|
||||
<span class="category-label"><i></i>{piece.category}</span>
|
||||
<span class:ready={piece.status === 'ready'} class="status-label"
|
||||
>{statusLabel[piece.status]}</span
|
||||
>
|
||||
</div>
|
||||
<h2>{piece.title}</h2>
|
||||
<p>{piece.summary}</p>
|
||||
<div class="piece-tags">
|
||||
<span>{piece.format}</span>
|
||||
{#each piece.topics as topic (topic)}<span>{topic}</span>{/each}
|
||||
</div>
|
||||
<footer>
|
||||
<span><b>{formatClock(piece.durationSeconds)}</b> geplant</span>
|
||||
<span>{piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'}</span>
|
||||
</footer>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<h2>Kein passendes Stück</h2>
|
||||
<p>Suche oder Filter zurücksetzen, um wieder alle Beispieldaten zu sehen.</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
query = '';
|
||||
filter = 'all';
|
||||
}}>Filter zurücksetzen</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getDatabase } from '#lib/server/database.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export function GET() {
|
||||
try {
|
||||
getDatabase().prepare('SELECT 1').get();
|
||||
return json({ status: 'ok' });
|
||||
} catch (error) {
|
||||
console.error('Healthcheck failed', error);
|
||||
return json({ status: 'error' }, { status: 503 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getShow } from '#lib/server/previews.js';
|
||||
|
||||
export function load() {
|
||||
const show = getShow();
|
||||
const usages = show.sections.flatMap((section) =>
|
||||
section.usages
|
||||
.filter((usage) => !usage.isReserve)
|
||||
.map((usage) => ({ ...usage, section: section.name }))
|
||||
);
|
||||
const currentIndex = Math.max(
|
||||
0,
|
||||
usages.findIndex((usage) => usage.status === 'current')
|
||||
);
|
||||
return { show, current: usages[currentIndex], next: usages[currentIndex + 1] ?? null };
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import PreviewNotice from '#lib/components/PreviewNotice.svelte';
|
||||
import { formatClock } from '#lib/format.js';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let promptSize = $state(1.55);
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Live-Vorschau | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap live-page">
|
||||
<header class="live-header">
|
||||
<div>
|
||||
<p class="eyebrow"><span class="live-pulse"></span> Live-Modus · Vorschau</p>
|
||||
<h1>{data.show.title}</h1>
|
||||
</div>
|
||||
<div class="live-header-actions">
|
||||
<div class="font-control" aria-label="Prompter-Schriftgröße">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (promptSize = Math.max(1.15, promptSize - 0.1))}
|
||||
aria-label="Schrift verkleinern">A−</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (promptSize = Math.min(2.1, promptSize + 0.1))}
|
||||
aria-label="Schrift vergrößern">A+</button
|
||||
>
|
||||
</div>
|
||||
<a href={resolve('/sendungen')}>Planung schließen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PreviewNotice area="Live-Modus" />
|
||||
|
||||
<div class="live-grid">
|
||||
<section class="live-current" style={`--prompt-size: ${promptSize}rem`}>
|
||||
<div class="live-kicker">
|
||||
<span>Aktuell · {data.current.section}</span><strong
|
||||
>{formatClock(data.current.durationSeconds)}</strong
|
||||
>
|
||||
</div>
|
||||
<h2>{data.current.title}</h2>
|
||||
<div class="prompter-copy">{data.current.intro}</div>
|
||||
<div class="moderation-card">
|
||||
<span>Moderationsstichpunkte</span>
|
||||
<p>{data.current.moderation}</p>
|
||||
</div>
|
||||
<details class="research-details">
|
||||
<summary>Recherche öffnen</summary>
|
||||
<p>
|
||||
Die Recherche bleibt im echten Live-Modus standardmäßig geschlossen und bewahrt beim
|
||||
Öffnen die Leseposition.
|
||||
</p>
|
||||
</details>
|
||||
<div class="transition-card">
|
||||
<span>Überleitung</span>
|
||||
<p>{data.current.transition}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="live-rail">
|
||||
<div class="next-card">
|
||||
<span>Als Nächstes</span>
|
||||
{#if data.next}
|
||||
<h3>{data.next.title}</h3>
|
||||
<p>{data.next.section} · {formatClock(data.next.durationSeconds)}</p>
|
||||
{:else}
|
||||
<h3>Sendungsende</h3>
|
||||
<p>Kein weiteres Stück eingeplant.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="live-progress">
|
||||
<header><span>Ablauf</span><strong>2 / 4</strong></header>
|
||||
<div class="progress-track"><span></span></div>
|
||||
{#each data.show.sections as section, index (section.id)}
|
||||
<div class:active={index === 1} class="rail-section">
|
||||
<span>{String(index + 1).padStart(2, '0')}</span>
|
||||
<div>
|
||||
<strong>{section.name}</strong><small>{formatClock(section.durationSeconds)}</small>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="readonly-actions" aria-label="Spätere Live-Aktionen">
|
||||
<span>Zurück</span><strong>In Etappe 5 steuerbar</strong><span>Erledigen</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getPieces, getShow } from '#lib/server/previews.js';
|
||||
|
||||
export function load() {
|
||||
return { pieces: getPieces(), show: getShow() };
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import PreviewNotice from '#lib/components/PreviewNotice.svelte';
|
||||
import { formatClock, formatDuration } from '#lib/format.js';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let mobilePane = $state<'fundus' | 'plan'>('plan');
|
||||
let plannedCount = $derived(
|
||||
data.show.sections.reduce(
|
||||
(total, section) => total + section.usages.filter((usage) => !usage.isReserve).length,
|
||||
0
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Sendungen | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap builder-page">
|
||||
<header class="page-heading show-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Nächste Sendung · {data.show.startsAtBerlin} Uhr</p>
|
||||
<h1>{data.show.title}</h1>
|
||||
</div>
|
||||
<div class="show-summary">
|
||||
<div><strong>{plannedCount}</strong><span>Stücke</span></div>
|
||||
<div>
|
||||
<strong>{formatDuration(data.show.totalDurationSeconds)}</strong><span>Planzeit</span>
|
||||
</div>
|
||||
<a class="live-link" href={resolve('/live')}>Live-Vorschau <span>↗</span></a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PreviewNotice area="Sendungsbau" />
|
||||
|
||||
<div class="mobile-pane-switch" aria-label="Arbeitsbereich auswählen">
|
||||
<button
|
||||
class:active={mobilePane === 'fundus'}
|
||||
type="button"
|
||||
onclick={() => (mobilePane = 'fundus')}>Fundus</button
|
||||
>
|
||||
<button class:active={mobilePane === 'plan'} type="button" onclick={() => (mobilePane = 'plan')}
|
||||
>Ablauf</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="builder-workspace">
|
||||
<section
|
||||
class="builder-pane source-pane"
|
||||
class:mobile-hidden={mobilePane !== 'fundus'}
|
||||
aria-labelledby="source-title"
|
||||
>
|
||||
<header class="pane-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Quelle</p>
|
||||
<h2 id="source-title">Sendefertiger Fundus</h2>
|
||||
</div>
|
||||
<span>{data.pieces.filter((piece) => piece.status === 'ready').length}</span>
|
||||
</header>
|
||||
<label class="compact-search">
|
||||
<span aria-hidden="true">⌕</span><span class="sr-only">Fundus durchsuchen</span>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Fundus durchsuchen"
|
||||
disabled
|
||||
title="Die vollständige Suche folgt in Etappe 3"
|
||||
/>
|
||||
</label>
|
||||
<div class="source-list">
|
||||
{#each data.pieces.filter((piece) => piece.status === 'ready') as piece (piece.id)}
|
||||
<article class="source-card" style={`--category: ${piece.categoryColor}`}>
|
||||
<div>
|
||||
<span class="category-label"><i></i>{piece.category}</span><span
|
||||
>{formatClock(piece.durationSeconds)}</span
|
||||
>
|
||||
</div>
|
||||
<h3>{piece.title}</h3>
|
||||
<p>{piece.format} · {piece.topics.join(', ')}</p>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="reserve-box">
|
||||
<div>
|
||||
<span aria-hidden="true">◇</span>
|
||||
<div><strong>Reservebank</strong><small>1 Stück vorbereitet</small></div>
|
||||
</div>
|
||||
<p>„Pixelart, die nicht nostalgisch sein will“ wartet als Ersatz.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="builder-pane run-pane"
|
||||
class:mobile-hidden={mobilePane !== 'plan'}
|
||||
aria-labelledby="run-title"
|
||||
>
|
||||
<header class="pane-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Sendungsplan</p>
|
||||
<h2 id="run-title">Ablauf</h2>
|
||||
</div>
|
||||
<span>{formatDuration(data.show.totalDurationSeconds)}</span>
|
||||
</header>
|
||||
|
||||
<div class="run-list">
|
||||
{#each data.show.sections as section, index (section.id)}
|
||||
<article class="run-section" style={`--category: ${section.categoryColor}`}>
|
||||
<header>
|
||||
<div class="section-number">{String(index + 1).padStart(2, '0')}</div>
|
||||
<div>
|
||||
<span class="category-label"><i></i>{section.category}</span>
|
||||
<h3>{section.name}</h3>
|
||||
</div>
|
||||
<strong>{formatClock(section.durationSeconds)}</strong>
|
||||
</header>
|
||||
{#if section.usages.filter((usage) => !usage.isReserve).length > 0}
|
||||
<div class="usage-list">
|
||||
{#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)}
|
||||
<div class="usage-row">
|
||||
<span class="drag-hint" aria-hidden="true">⠿</span>
|
||||
<div>
|
||||
<strong>{usage.title}</strong><small
|
||||
>{formatClock(usage.durationSeconds)} · Eigene Sendungskopie</small
|
||||
>
|
||||
</div>
|
||||
<span class:current={usage.status === 'current'} class="usage-status"
|
||||
>{usage.status === 'current' ? 'Aktuell' : 'Offen'}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="section-placeholder">
|
||||
Eigenständiger Spieleblock · keine Piece-Kopie nötig
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getTaxonomy } from '#lib/server/previews.js';
|
||||
|
||||
export function load() {
|
||||
return { taxonomy: getTaxonomy() };
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Verwaltung | Sendewerk</title></svelte:head>
|
||||
|
||||
<div class="page-wrap narrow-page">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">System & Struktur</p>
|
||||
<h1>Verwaltung</h1>
|
||||
<p class="page-intro">
|
||||
Die bereits angelegte Taxonomie ist sichtbar; Bearbeitung und Benutzerverwaltung folgen mit
|
||||
serverseitigen Rechten.
|
||||
</p>
|
||||
</header>
|
||||
<div class="settings-grid">
|
||||
<section class="quiet-panel taxonomy-panel">
|
||||
<p class="eyebrow">Abschnittskategorien</p>
|
||||
<h2>Struktur im Ablauf</h2>
|
||||
<div class="taxonomy-list">
|
||||
{#each data.taxonomy.categories as category (category.id)}<span
|
||||
><i style={`--tax-color: ${category.color}`}></i>{category.name}</span
|
||||
>{/each}
|
||||
</div>
|
||||
</section>
|
||||
<section class="quiet-panel taxonomy-panel">
|
||||
<p class="eyebrow">Formate</p>
|
||||
<h2>Form des Stücks</h2>
|
||||
<div class="taxonomy-list">
|
||||
{#each data.taxonomy.formats as format (format.id)}<span>{format.name}</span>{/each}
|
||||
</div>
|
||||
</section>
|
||||
<section class="quiet-panel taxonomy-panel">
|
||||
<p class="eyebrow">Themen</p>
|
||||
<h2>Inhaltliche Zuordnung</h2>
|
||||
<div class="taxonomy-list">
|
||||
{#each data.taxonomy.topics as topic (topic.id)}<span>{topic.name}</span>{/each}
|
||||
</div>
|
||||
</section>
|
||||
<section class="quiet-panel planned-settings">
|
||||
<p class="eyebrow">Etappe 2</p>
|
||||
<h2>Konten & Rollen</h2>
|
||||
<p>
|
||||
Barkeeper, Thekencrew und technische Zugänge werden erst mit vollständiger Authentifizierung
|
||||
verwaltbar.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user