Add Logbuch: project update blog with admin, media and API

Public archive with sidebar navigation, project pages, month archive,
search and entry pages with image blocks. Admin area for brands,
projects, post types, users, api clients, media and the entry editor
with drag and drop images, preview per audience, scheduling and
publish checks. Read and write API with bearer tokens, audience
scoping, idempotent creation, OpenAPI document and editorial guide.
Magic link login with configurable allowed domains, whole app behind
the session gate. 456 tests including design rule checks.
This commit is contained in:
Matthias Giesselmann
2026-07-31 21:33:42 +02:00
commit b90ff252d1
291 changed files with 43671 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
# Logbuch Gestaltungsrichtung
Stand: 30.07.2026. Diese Datei ist verbindlich. Jede Farb-, Schrift- und Abstandsentscheidung wird hieraus abgeleitet, nichts wird zusätzlich erfunden.
## Warum der erste Entwurf nicht trug
Der erste Entwurf bestand aus Haarlinien, Mono-Kleinkram und einer sehr großen Überschrift. Er funktionierte nur unter der Annahme, dass jeder Eintrag ein Bild mitbringt. Ohne Bilder blieb eine graue Liste ohne Halt. Die Gestaltung muss vollständig ohne Bildmaterial tragen und mit Bildmaterial besser werden, nicht umgekehrt.
## Leitgedanke
Logbuch zeigt Meldungen aus einer Flotte von Projekten. Die wichtigste Information eines Eintrags ist nicht sein Datum, sondern **von welchem Projekt er kommt**. Deshalb trägt die Projektfarbe die Seite.
## Signaturelement: die Kennplatte
Ein massives Farbfeld in der Projektfarbe mit dem Projektkürzel und der laufenden Eintragsnummer, gesetzt wie eine Rumpf- oder Containerbeschriftung.
```
┌──────────────┐
│ TRK │
│ 0142 │
└──────────────┘
```
- Kürzel aus `project.code`, Rückfall auf die ersten drei Buchstaben des Slugs in Großbuchstaben
- Nummer immer vierstellig mit führenden Nullen
- Schrift: Mono, 600, weite Laufweite beim Kürzel, sehr groß bei der Nummer
- Textfarbe auf der Platte: immer Papierfarbe, nie Tinte
- In der Liste als schmale Variante, im Aufmacher als große Fläche
Die Platte ist der einzige Ort, an dem kräftige Farbe vorkommt. Alles andere bleibt ruhig.
## Farbe
Grundpalette, hell:
| Token | Wert | Verwendung |
|---|---|---|
| `paper` | `#f2f1ec` | Seitenhintergrund |
| `surface` | `#fbfaf6` | abgesetzte Flächen |
| `ink` | `#14161a` | Titel, Fließtext |
| `ink-2` | `#4c5157` | Anreißer, Sekundärtext |
| `ink-3` | `#8a8f95` | Beschriftungen, Datum |
| `rule` | `#dedcd3` | Trennlinien |
| `signal` | `#c43a22` | ausschließlich für Neu-Markierung und die Randlinie |
Dunkel, dieselben Tokennamen:
| Token | Wert |
|---|---|
| `paper` | `#121417` |
| `surface` | `#191c20` |
| `ink` | `#edebe4` |
| `ink-2` | `#a9aeb4` |
| `ink-3` | `#757a80` |
| `rule` | `#2a2e34` |
| `signal` | `#e4603f` |
Projektfarben kommen aus der Datenbank, `project.color`. Sie werden nur auf Kennplatten und als Balken verwendet, niemals als Textfarbe auf Papier. Im dunklen Modus wird die Plattenfarbe über `color-mix` um 12 Prozent zur Papierfarbe abgedunkelt, damit sie nicht leuchtet.
Verboten: Farbverläufe, Schlagschatten außer einem einzigen für den Aufmacher, farbige Titel. Titel sind immer `ink`, auch wenn sie Verweise sind. Verweisblau gilt nur für echte Verweise im Fließtext.
## Schrift
Selbst gehostet über `next/font/google`, keine Systemschriften mehr, damit es auf jedem Rechner gleich aussieht.
| Rolle | Schrift | Einsatz |
|---|---|---|
| Display | Archivo, 600 und 700, Laufweite -0.02em | Titel, Wortmarke, Abschnittsköpfe |
| Fließtext | Source Serif 4, 400, Zeilenhöhe 1.6 | Anreißer, Artikeltext |
| Technisch | JetBrains Mono, 500 | Kennplatten, Datum, Nummern, Beschriftungen |
Skala in rem, keine Zwischenwerte erfinden:
| Stufe | Größe | Verwendung |
|---|---|---|
| micro | 0.6875 | Beschriftungen in Versalien |
| small | 0.8125 | Datum, Metazeilen |
| base | 1.0625 | Fließtext |
| lead | 1.25 | Anreißer im Aufmacher, Titel in der Liste |
| title | 1.75 | Abschnittsköpfe, Titel auf Projektseiten |
| hero | clamp(2.25, 4vw, 3.25) | Aufmachertitel, Seitentitel |
| plate | clamp(2, 5vw, 3.5) | Nummer auf der großen Kennplatte |
## Nachtrag 30.07.2026: Aufbau nach dem Vergleich
Ein zweiter Entwurf hat drei Dinge besser gelöst. Sie sind ab sofort verbindlich und stechen die Skizze weiter unten.
**Feste Seitenleiste statt Filterzeile.** Links eine schmale Spalte über die volle Höhe: Wortmarke mit dem Stand des Logbuchs, Suche, die Einträge Übersicht und Archiv, dann alle Projekte mit Farbpunkt, gruppiert nach Marke. Unten der Hell-Dunkel-Umschalter. Die Projekte sind Daten, die Leiste wächst mit ihnen. Ab Tablettbreite klappt sie zu einer Kopfzeile mit Schublade zusammen.
**Der Aufmacher trägt ein Bild.** Volle Breite des Inhaltsbereichs, darunter Projektzeile, Titel, Anreißer, Autor. Die Eintragsnummer steht sehr groß und sehr blass hinter dem Text, als Wasserzeichen. Ohne Bild übernimmt die Kennplattenfläche denselben Platz, das Wasserzeichen bleibt.
**Zweite Spalte rechts.** Neben dem Aufmacher stehen die zwei bis drei nächsten Einträge als kleine Karten mit Vorschaubild, Kürzel, Art, Titel und Anreißerbeginn.
**Zeilen zeigen mehr.** Eine Listenzeile trägt Kürzel und Nummer, die Art, den Titel, den Anreißer in einer Zeile, rechts Datum und Autor. Nicht nur Titel und Datum.
**Kürzel-Schreibweise.** Kürzel und Nummer werden als `TRK-0142` gesetzt, mit Bindestrich, nicht als zwei getrennte Angaben.
## Aufbau der Übersicht
```
┌───────────────────────────────────────────────────────────────┐
│ LOGBUCH alle · trakk · mta360 · ... suchen ☀ │ schmal, klebend
├───────────────────────────────────────────────────────────────┤
│ ZULETZT 7 Einträge · 14 Tage │ Augenbraue
│ │
│ ┌───────────────┬───────────────────────────────────────────┐ │
│ │ TRK │ FEATURE NEU │ │ Aufmacher
│ │ │ Regel-Engine: Bedingung trifft Aktion │ │
│ │ 0142 │ Anreißer in Serifenschrift, zwei Zeilen │ │
│ │ │ 30.07.2026 · Paul · Trakk │ │
│ └───────────────┴───────────────────────────────────────────┘ │
│ │
│ ── ÄLTERE EINTRÄGE ────────────────────────────────────── │
│ ▌MTA 0141 Spalten per Rechtsklick verwalten 29.07. │ dichte Zeilen
│ ▌TEL 0140 10.000 Tracker auf einer Karte 28.07. │
│ ▌TRK 0139 SLA-Uhr zählt Feiertage nicht mit 25.07. │
│ │
│ ── ARCHIV ─────────────────────────────────────────────── │
│ 2026 [Jan 14] [Feb 11] [Mär 18] ... │
└───────────────────────────────────────────────────────────────┘
```
Der Aufmacher ist genau ein Eintrag, der neueste. Alles darunter ist dicht und zum Überfliegen gebaut: eine Zeile pro Eintrag, links ein Balken in der Projektfarbe, dann Kürzel und Nummer in Mono, dann der Titel in Display, rechts das Datum. Bei Überfahren wächst der Balken und der Titel rückt zwei Pixel nach rechts.
Sechs Einträge müssen ohne Scrollen sichtbar sein. Der erste Entwurf brauchte für vier Einträge einen ganzen Bildschirm, das war der Kern des Problems.
## Aufbau der Beitragsseite
Reihenfolge: Kennplatte klein und Projektzeile, dann Titel, dann Anreißer als Lead in `lead`, dann der Inhalt in einer Lesespalte von 38rem. Metadaten stehen **nicht** zwischen Titel und Text, sondern als eine ruhige Zeile unter dem Titel und ausführlich am Fuß des Artikels.
Der Anreißer wird nie doppelt gezeigt. Wenn der erste Inhaltsblock denselben Text enthält, wird der Block übersprungen.
## Fehlende Bilder
Bilder gibt es noch nicht. Ein Eintrag ohne Bild bekommt deshalb im Aufmacher die große Kennplatte als Fläche, nicht einen leeren Rahmen und keinen Platzhalter mit Kamerasymbol. Sobald ein Beitrag ein Aufmacherbild hat, tritt die Platte auf die schmale Variante zurück und das Bild übernimmt die Fläche.
## Bewegung
Sparsam. Beim Laden erscheinen die Listenzeilen versetzt um je 40 Millisekunden mit 6 Pixel Versatz. Beim Überfahren wächst der Farbbalken. Sonst nichts. Bei `prefers-reduced-motion` entfällt alles.
## Sorgfalt im Detail
Der Auftraggeber hat den ersten Bau nicht als kaputt bezeichnet, sondern als lieblos. Das ist die genauere Kritik. Struktur allein reicht nicht, die folgenden Kleinigkeiten sind Pflicht, nicht Kür:
- **Zahlen laufen tabellarisch.** `font-variant-numeric: tabular-nums` überall, wo Nummern und Daten untereinander stehen. Eine Liste, in der die Nummern zittern, sieht ungepflegt aus.
- **Datum spricht wie ein Mensch.** Jünger als sieben Tage: "vor drei Tagen". Älter: das Datum. Im Titelattribut immer das genaue Datum.
- **Lesedauer** je Beitrag, geschätzt aus der Textmenge, in der Metazeile. Sagt dem Leser, worauf er sich einlässt.
- **Autor als Kürzel-Marke**, zwei Buchstaben in einem kleinen Feld in Tintenfarbe. Kein Bild, keine leere Silhouette.
- **Leerzustände sagen, was als Nächstes zu tun ist.** Nicht "Keine Einträge", sondern was der Leser jetzt tun kann, mit einem Weg dorthin.
- **Trennlinien nur zwischen Gleichartigem.** Der letzte Eintrag einer Liste bekommt keine Linie nach unten.
- **Überschriften brechen ausgeglichen**, `text-wrap: balance` für Titel, `pretty` für Fließtext. Keine einzelnen Wörter in der letzten Zeile.
- **Textauswahl** trägt die Signalfarbe, nicht das Browserblau.
- **Sichtbarer Tastaturfokus** in Signalfarbe, nicht der Standardrahmen des Browsers.
- **Überfahren fühlt sich an**: der Farbbalken wächst, der Titel rückt zwei Pixel, beides in 120 Millisekunden.
- **Die 404-Seite** ist gestaltet und trägt eine eigene Zeile, keine Standardmeldung.
- **Der Aufmacher bekommt genau einen Schatten**, sehr weich, damit er von der Fläche abhebt. Sonst gibt es im ganzen Entwurf keine Schatten.
Wenn eine dieser Kleinigkeiten fehlt, ist die Arbeit nicht fertig, auch wenn die Seite lädt.
## Qualitätsboden
Bis 22rem Breite benutzbar, sichtbarer Tastaturfokus, Kontrast mindestens 4.5 zu 1 für Fließtext, keine nativen Scrollbars, hell und dunkel aus denselben Tokens.
+546
View File
@@ -0,0 +1,546 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logbuch</title>
<style>
:root {
--paper: #eff0ec;
--surface: #fbfbf9;
--ink: #191c20;
--ink-2: #4a4f56;
--ink-3: #868b92;
--rule: #d8d9d2;
--red: #c43a22;
--blue: #2b4a9b;
--shot-1: #dcdcd5;
--shot-2: #c9cac2;
--font-display: "Futura", "Avenir Next Condensed", "Avenir Next", system-ui, sans-serif;
--font-body: "Charter", "Iowan Old Style", Palatino, Georgia, serif;
--font-mono: "SF Mono", ui-monospace, Menlo, monospace;
--margin-col: 6rem;
--gap: 1.5rem;
--maxw: 63rem;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #15171a;
--surface: #1c1f23;
--ink: #e9e8e3;
--ink-2: #a8adb4;
--ink-3: #73787f;
--rule: #2c3036;
--red: #e46045;
--blue: #8aa6ee;
--shot-1: #2a2e34;
--shot-2: #383d45;
}
}
:root[data-theme="dark"] {
--paper: #15171a;
--surface: #1c1f23;
--ink: #e9e8e3;
--ink-2: #a8adb4;
--ink-3: #73787f;
--rule: #2c3036;
--red: #e46045;
--blue: #8aa6ee;
--shot-1: #2a2e34;
--shot-2: #383d45;
}
:root[data-theme="light"] {
--paper: #eff0ec;
--surface: #fbfbf9;
--ink: #191c20;
--ink-2: #4a4f56;
--ink-3: #868b92;
--rule: #d8d9d2;
--red: #c43a22;
--blue: #2b4a9b;
--shot-1: #dcdcd5;
--shot-2: #c9cac2;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--font-body);
font-size: 1rem;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, .label, .btn, .badge, .stamp { font-family: var(--font-display); }
.label, .meta, .num { font-family: var(--font-mono); }
a { color: var(--blue); text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
::-webkit-scrollbar { width: 0.5rem; height: 0.5rem; }
::-webkit-scrollbar-thumb { background: var(--ink-3); border-radius: 1rem; }
::-webkit-scrollbar-track { background: transparent; }
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 1.5rem; }
/* Kopfleiste */
.bar {
position: sticky; top: 0; z-index: 5;
display: flex; align-items: center; gap: 1rem;
padding: 0.75rem 1.5rem;
background: color-mix(in srgb, var(--paper) 88%, transparent);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--rule);
}
.mark {
font-family: var(--font-display);
font-size: 0.95rem; font-weight: 500;
letter-spacing: 0.42em; text-transform: uppercase;
margin-right: auto;
}
.mark span { color: var(--red); }
.btn {
font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase;
padding: 0.45rem 0.8rem; border: 1px solid var(--rule);
background: var(--surface); color: var(--ink); border-radius: 0.125rem;
cursor: pointer;
}
.btn:hover { border-color: var(--ink-3); }
.btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.btn--primary:hover { background: var(--red); border-color: var(--red); }
/* Formularkopf */
.masthead { padding: 3.5rem 0 2.5rem; }
.masthead h1 {
font-size: clamp(2.5rem, 7vw, 4.5rem);
font-weight: 500; line-height: 0.95; letter-spacing: -0.01em;
margin: 0 0 0.5rem;
}
.masthead p {
margin: 0 0 2.25rem; max-width: 34rem;
color: var(--ink-2); font-size: 1.0625rem;
}
.fields { display: grid; gap: 0.4rem; }
.field { display: flex; align-items: baseline; gap: 0.6rem; font-size: 0.75rem; }
.field .label { color: var(--ink-3); letter-spacing: 0.16em; text-transform: uppercase; white-space: nowrap; }
.field .leader { flex: 1; border-bottom: 1px dotted var(--rule); }
.field .val { font-family: var(--font-mono); color: var(--ink-2); text-align: right; }
/* Filterzeile */
.filters { display: flex; flex-wrap: wrap; gap: 0.4rem; padding: 1.75rem 0 0.5rem; }
.chip {
display: inline-flex; align-items: center; gap: 0.45rem;
font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em;
padding: 0.35rem 0.65rem; border: 1px solid var(--rule); border-radius: 1rem;
background: transparent; color: var(--ink-2); cursor: pointer;
}
.chip[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.chip i { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--dot, var(--ink-3)); }
/* Logbuch-Schiene */
.log { position: relative; padding: 1rem 0 3rem; }
.log::before {
content: ""; position: absolute; top: 0; bottom: 0;
left: calc(var(--margin-col) - 0.5rem); width: 1px;
background: color-mix(in srgb, var(--red) 45%, transparent);
}
.entry {
display: grid; grid-template-columns: var(--margin-col) 1fr; gap: var(--gap);
padding: 1.75rem 0; border-bottom: 1px solid var(--rule);
animation: rise 0.5s both cubic-bezier(0.22, 0.61, 0.36, 1);
animation-delay: calc(var(--i, 0) * 60ms);
}
.entry:last-child { border-bottom: 0; }
@keyframes rise { from { opacity: 0; transform: translateY(0.5rem); } }
@media (prefers-reduced-motion: reduce) { .entry { animation: none; } }
.entry .rail { position: relative; text-align: right; }
.entry .rail::after {
content: ""; position: absolute; right: -0.75rem; top: 0.45rem;
width: 0.5rem; height: 0.5rem;
border: 1px solid var(--red); background: var(--paper);
transition: background 0.15s;
}
.entry:hover .rail::after { background: var(--red); }
.entry--unread .rail::after { background: var(--red); }
.entry .date { display: block; font-family: var(--font-mono); font-size: 0.8125rem; color: var(--ink); }
.entry .year, .entry .num { display: block; font-family: var(--font-mono); font-size: 0.6875rem; color: var(--ink-3); }
.entry .num { margin-top: 0.5rem; }
.head { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
.badge {
font-size: 0.625rem; letter-spacing: 0.16em; text-transform: uppercase;
padding: 0.2rem 0.45rem; border: 1px solid currentColor; color: var(--ink-2);
}
.badge--feature { color: var(--blue); }
.badge--breaking { color: var(--red); }
.stamp {
margin-left: auto; transform: rotate(-5deg);
font-size: 0.6875rem; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--red); border: 0.125rem solid var(--red); padding: 0.15rem 0.5rem;
opacity: 0.85;
}
.entry h2 { margin: 0 0 0.5rem; font-size: 1.375rem; font-weight: 500; line-height: 1.2; }
.entry--lead h2 { font-size: clamp(1.75rem, 3.6vw, 2.5rem); }
.entry p { margin: 0; color: var(--ink-2); max-width: 40rem; }
.foot {
display: flex; align-items: center; gap: 0.75rem; margin-top: 0.9rem;
font-family: var(--font-mono); font-size: 0.6875rem; color: var(--ink-3);
}
.foot .proj { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--ink-2); }
.foot .proj i { width: 0.5rem; height: 0.5rem; background: var(--dot); }
.foot .sep { color: var(--rule); }
/* Bildplatzhalter */
.shots { display: flex; gap: 0.5rem; margin-top: 1rem; }
.shot {
position: relative; overflow: hidden;
background: var(--surface); border: 1px solid var(--rule); border-radius: 0.1875rem;
flex: 1; aspect-ratio: 16 / 10;
}
.shot--wide { aspect-ratio: 21 / 9; }
.shot::before {
content: ""; position: absolute; inset: 0.5rem 0.5rem auto; height: 0.375rem;
background: var(--shot-2); border-radius: 0.125rem;
}
.shot--table::after {
content: ""; position: absolute; inset: 1.4rem 0.5rem 0.5rem;
background:
linear-gradient(var(--shot-1) 0 0) 0 0 / 62% 0.3rem no-repeat,
linear-gradient(var(--shot-1) 0 0) 0 0.75rem / 88% 0.3rem no-repeat,
linear-gradient(var(--shot-1) 0 0) 0 1.5rem / 45% 0.3rem no-repeat,
linear-gradient(var(--shot-1) 0 0) 0 2.25rem / 74% 0.3rem no-repeat,
linear-gradient(var(--shot-1) 0 0) 0 3rem / 55% 0.3rem no-repeat;
}
.shot--chart::after {
content: ""; position: absolute; inset: auto 0.5rem 0.5rem; height: 55%;
background:
linear-gradient(var(--shot-2) 0 0) 0 100% / 12% 40% no-repeat,
linear-gradient(var(--shot-2) 0 0) 22% 100% / 12% 72% no-repeat,
linear-gradient(var(--shot-2) 0 0) 44% 100% / 12% 55% no-repeat,
linear-gradient(var(--shot-2) 0 0) 66% 100% / 12% 90% no-repeat,
linear-gradient(var(--shot-2) 0 0) 88% 100% / 12% 30% no-repeat;
}
.shot--map::after {
content: ""; position: absolute; inset: 1.4rem 0.5rem 0.5rem;
background:
radial-gradient(circle 0.15rem, var(--red) 98%, transparent) 18% 30% / 100% 100% no-repeat,
radial-gradient(circle 0.15rem, var(--red) 98%, transparent) 42% 62% / 100% 100% no-repeat,
radial-gradient(circle 0.15rem, var(--red) 98%, transparent) 71% 24% / 100% 100% no-repeat,
radial-gradient(circle 0.15rem, var(--shot-2) 98%, transparent) 58% 80% / 100% 100% no-repeat,
radial-gradient(circle 0.15rem, var(--shot-2) 98%, transparent) 86% 58% / 100% 100% no-repeat,
linear-gradient(var(--shot-1) 0 0) 0 45% / 100% 1px no-repeat;
}
/* Archiv */
.section-label {
display: flex; align-items: center; gap: 1rem; margin: 0 0 1.25rem;
font-family: var(--font-display); font-size: 0.7rem;
letter-spacing: 0.24em; text-transform: uppercase; color: var(--ink-3);
}
.section-label::after { content: ""; flex: 1; height: 1px; background: var(--rule); }
.archive { padding: 2.5rem 0; border-top: 1px solid var(--rule); }
.year { display: grid; grid-template-columns: 4rem 1fr; gap: var(--gap); margin-bottom: 1.25rem; }
.year > .label { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--ink-3); }
.months { display: flex; flex-wrap: wrap; gap: 0.375rem; }
.month {
display: flex; align-items: baseline; gap: 0.4rem;
padding: 0.3rem 0.55rem; border: 1px solid var(--rule); border-radius: 0.125rem;
font-family: var(--font-mono); font-size: 0.7rem; color: var(--ink-2);
background: transparent; cursor: pointer;
}
.month:hover { border-color: var(--red); color: var(--ink); }
.month b { font-weight: 400; color: var(--ink-3); font-size: 0.625rem; }
/* In-App-Vorschau */
.inapp { padding: 2.5rem 0 4rem; border-top: 1px solid var(--rule); }
.inapp .hint { color: var(--ink-2); max-width: 34rem; margin: 0 0 1.5rem; font-size: 0.9375rem; }
.panel {
max-width: 24rem; background: var(--surface);
border: 1px solid var(--rule); border-radius: 0.25rem;
box-shadow: 0 1.5rem 3rem -1.5rem color-mix(in srgb, var(--ink) 25%, transparent);
overflow: hidden;
}
.panel header {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.75rem 0.875rem; border-bottom: 1px solid var(--rule);
font-family: var(--font-display); font-size: 0.8125rem;
}
.panel header .count {
font-family: var(--font-mono); font-size: 0.625rem;
background: var(--red); color: #fff; padding: 0.1rem 0.35rem; border-radius: 1rem;
}
.panel header .close { margin-left: auto; color: var(--ink-3); font-family: var(--font-mono); }
.panel ul { list-style: none; margin: 0; padding: 0; max-height: 15rem; overflow-y: auto; }
.panel li { padding: 0.75rem 0.875rem; border-bottom: 1px solid var(--rule); }
.panel li:last-child { border-bottom: 0; }
.panel li .meta { font-size: 0.625rem; color: var(--ink-3); letter-spacing: 0.06em; }
.panel li strong { display: block; font-family: var(--font-display); font-weight: 500; font-size: 0.9375rem; margin: 0.2rem 0; }
.panel li p { margin: 0; font-size: 0.8125rem; color: var(--ink-2); }
.panel footer { display: flex; gap: 0.5rem; padding: 0.75rem 0.875rem; border-top: 1px solid var(--rule); }
footer.site {
padding: 1.5rem 0 3rem; border-top: 1px solid var(--rule);
display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;
font-family: var(--font-mono); font-size: 0.6875rem; color: var(--ink-3);
}
footer.site .mark { font-size: 0.7rem; letter-spacing: 0.3em; margin-right: auto; color: var(--ink-2); }
@media (max-width: 46rem) {
.log::before { display: none; }
.entry { grid-template-columns: 1fr; gap: 0.6rem; }
.entry .rail { display: flex; gap: 0.75rem; text-align: left; }
.entry .rail::after { display: none; }
.entry .date, .entry .year, .entry .num { display: inline; }
.entry .num { margin: 0; }
.year { grid-template-columns: 1fr; gap: 0.5rem; }
.field .val { text-align: left; }
}
</style>
</head>
<body>
<div class="bar">
<div class="mark">Log<span>b</span>uch</div>
<button class="btn" id="theme" aria-label="Farbmodus wechseln">Hell / Dunkel</button>
<button class="btn btn--primary">Eintrag schreiben</button>
</div>
<div class="wrap">
<section class="masthead">
<h1>Was zuletzt<br>passiert ist.</h1>
<p>Alle zwei Wochen halten wir pro Projekt fest, was fertig geworden ist. Mit Bildern, in ganzen Sätzen, ohne Ticketnummern-Kauderwelsch.</p>
<div class="fields">
<div class="field"><span class="label">Flotte</span><span class="leader"></span><span class="val">NYO · Pocket Rocket · Tajo · Aliens Exist</span></div>
<div class="field"><span class="label">Zeitraum</span><span class="leader"></span><span class="val">01.01.2026 - 30.07.2026</span></div>
<div class="field"><span class="label">Einträge</span><span class="leader"></span><span class="val">142 gesamt, 6 in den letzten 14 Tagen</span></div>
<div class="field"><span class="label">Stand</span><span class="leader"></span><span class="val">30.07.2026, 09:14</span></div>
</div>
<div class="filters">
<button class="chip" aria-pressed="true">Alle Projekte</button>
<button class="chip" aria-pressed="false" style="--dot:#2e7d5b"><i></i>Trakk</button>
<button class="chip" aria-pressed="false" style="--dot:#2b4a9b"><i></i>MTA360</button>
<button class="chip" aria-pressed="false" style="--dot:#b4761a"><i></i>MTA Telematik</button>
<button class="chip" aria-pressed="false" style="--dot:#6b4ba8"><i></i>Orbit</button>
<button class="chip" aria-pressed="false" style="--dot:#a33b6a"><i></i>Pulsar</button>
</div>
</section>
<main class="log">
<article class="entry entry--lead entry--unread" style="--i:0">
<div class="rail">
<span class="date">30.07.</span>
<span class="year">2026</span>
<span class="num">0142</span>
</div>
<div>
<div class="head">
<span class="badge badge--feature">Feature</span>
<span class="stamp">Neu</span>
</div>
<h2>Regel-Engine: Bedingung trifft Aktion</h2>
<p>Tickets reagieren jetzt selbst. Du setzt eine Bedingung und hängst eine Aktion daran: Status ändern, zuweisen, Nachricht schicken. Läuft über den Event-Bus im Worker, ohne Cronjobs.</p>
<div class="shots">
<div class="shot shot--wide shot--table"></div>
</div>
<div class="foot">
<span class="proj" style="--dot:#2e7d5b"><i></i>Trakk</span>
<span class="sep">/</span>
<span>3 Bilder</span>
<span class="sep">/</span>
<span>Paul</span>
</div>
</div>
</article>
<article class="entry entry--unread" style="--i:1">
<div class="rail">
<span class="date">29.07.</span>
<span class="year">2026</span>
<span class="num">0141</span>
</div>
<div>
<div class="head"><span class="badge">Verbesserung</span></div>
<h2>Spalten per Rechtsklick verwalten</h2>
<p>Rechtsklick auf eine Kopfzeile öffnet das Spaltenmenü: sortieren, anpinnen, ausblenden, Breite zurücksetzen. Wer das nicht mag, schaltet es in den Einstellungen ab.</p>
<div class="shots">
<div class="shot shot--table"></div>
<div class="shot shot--chart"></div>
</div>
<div class="foot">
<span class="proj" style="--dot:#2b4a9b"><i></i>MTA360</span>
<span class="sep">/</span>
<span>2 Bilder</span>
</div>
</div>
</article>
<article class="entry entry--unread" style="--i:2">
<div class="rail">
<span class="date">28.07.</span>
<span class="year">2026</span>
<span class="num">0140</span>
</div>
<div>
<div class="head"><span class="badge badge--feature">Feature</span></div>
<h2>10.000 Tracker auf einer Karte</h2>
<p>Positionen kommen gebündelt aus TimescaleDB, Geofences werden serverseitig geprüft. Die Karte bleibt bei voller Flotte flüssig.</p>
<div class="shots">
<div class="shot shot--map"></div>
<div class="shot shot--chart"></div>
<div class="shot shot--table"></div>
</div>
<div class="foot">
<span class="proj" style="--dot:#b4761a"><i></i>MTA Telematik</span>
<span class="sep">/</span>
<span>3 Bilder</span>
</div>
</div>
</article>
<article class="entry" style="--i:3">
<div class="rail">
<span class="date">25.07.</span>
<span class="year">2026</span>
<span class="num">0139</span>
</div>
<div>
<div class="head"><span class="badge">Fix</span></div>
<h2>SLA-Uhr zählt Feiertage nicht mehr mit</h2>
<p>Reaktionszeiten liefen über Wochenenden und Feiertage weiter und lösten falsche Eskalationen aus. Der Kalender pro Kunde gilt jetzt auch für die Uhr.</p>
<div class="foot">
<span class="proj" style="--dot:#2e7d5b"><i></i>Trakk</span>
<span class="sep">/</span>
<span>gelesen</span>
</div>
</div>
</article>
<article class="entry" style="--i:4">
<div class="rail">
<span class="date">23.07.</span>
<span class="year">2026</span>
<span class="num">0138</span>
</div>
<div>
<div class="head"><span class="badge badge--breaking">Breaking</span></div>
<h2>Lokale Tabellen-Konfigurationen werden nicht mehr gelesen</h2>
<p>Ansichten liegen jetzt vollständig in der Datenbank. Wer noch eine Konfiguration im Browser hatte, sieht die Standardansicht und legt seine Ansicht einmal neu an.</p>
<div class="foot">
<span class="proj" style="--dot:#2b4a9b"><i></i>MTA360</span>
<span class="sep">/</span>
<span>gelesen</span>
</div>
</div>
</article>
<article class="entry" style="--i:5">
<div class="rail">
<span class="date">21.07.</span>
<span class="year">2026</span>
<span class="num">0137</span>
</div>
<div>
<div class="head"><span class="badge">Info</span></div>
<h2>Ein Eingang für alle Kommentare</h2>
<p>Kommentare aus Instagram, LinkedIn und Facebook landen in einem Eingang, Antworten gehen von dort zurück.</p>
<div class="foot">
<span class="proj" style="--dot:#6b4ba8"><i></i>Orbit</span>
<span class="sep">/</span>
<span>gelesen</span>
</div>
</div>
</article>
</main>
<section class="archive">
<h2 class="section-label">Archiv</h2>
<div class="year">
<span class="label">2026</span>
<div class="months">
<button class="month">Jan <b>14</b></button>
<button class="month">Feb <b>11</b></button>
<button class="month">Mär <b>18</b></button>
<button class="month">Apr <b>21</b></button>
<button class="month">Mai <b>19</b></button>
<button class="month">Jun <b>23</b></button>
<button class="month">Jul <b>16</b></button>
</div>
</div>
<div class="year">
<span class="label">2025</span>
<div class="months">
<button class="month">Okt <b>6</b></button>
<button class="month">Nov <b>9</b></button>
<button class="month">Dez <b>5</b></button>
</div>
</div>
</section>
<section class="inapp">
<h2 class="section-label">In der Anwendung</h2>
<p class="hint">Dieselben Einträge holt sich jede App über die API und zeigt sie im eigenen Design. Gelesen wird pro Nutzer der jeweiligen App gemerkt.</p>
<div class="panel">
<header>
Was ist neu in Trakk
<span class="count">3</span>
<span class="close">Esc</span>
</header>
<ul>
<li>
<span class="meta">30.07. · Feature</span>
<strong>Regel-Engine: Bedingung trifft Aktion</strong>
<p>Tickets reagieren jetzt selbst, ohne Cronjobs.</p>
</li>
<li>
<span class="meta">25.07. · Fix</span>
<strong>SLA-Uhr zählt Feiertage nicht mehr mit</strong>
<p>Keine falschen Eskalationen über Wochenenden.</p>
</li>
<li>
<span class="meta">18.07. · Verbesserung</span>
<strong>Anhänge direkt im Ticket</strong>
<p>Dateien landen per Drag &amp; Drop im Verlauf.</p>
</li>
</ul>
<footer>
<button class="btn btn--primary">Alles gelesen</button>
<button class="btn">Im Logbuch öffnen</button>
</footer>
</div>
</section>
<footer class="site">
<span class="mark">Logbuch</span>
<span>Spaceport Adventures</span>
<span class="sep">/</span>
<span>Entwurf 30.07.2026</span>
</footer>
</div>
<script>
const root = document.documentElement
document.getElementById('theme').addEventListener('click', () => {
const dark = getComputedStyle(root).getPropertyValue('--paper').trim() === '#15171a'
root.dataset.theme = dark ? 'light' : 'dark'
})
document.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.chip').forEach(c => c.setAttribute('aria-pressed', 'false'))
chip.setAttribute('aria-pressed', 'true')
})
})
</script>
</body>
</html>