monitor/html/css/office.css
fds2610-bot 55d5c34ca6 Ueberwachte Hosts ueber ein Zahnrad am System-Monitor pflegen (Issue #70)
Wer einen Server aus der Ping-Ueberwachung nehmen wollte, musste sich bisher auf den
Rechner mit check_fds.sh einloggen und die hosts.conf editieren - und die Kachel blieb
danach trotzdem im System-Monitor stehen, weil ein einmal gemeldetes System nirgends
wieder geloescht werden konnte.

Die Liste steht jetzt in der Datenbank und ist ueber ein Zahnrad neben der Ueberschrift
"Systeme" pflegbar, analog zum Zahnrad am PO-Platz (Issue #27). Jeder Host gehoert dem
OPs-Agenten (Issue #66), der ihn anpingt: API und Ping-Check laufen nicht zwangslaeufig
auf demselben Rechner, die API kann die hosts.conf also nicht selbst schreiben.
check_fds.sh holt sie stattdessen bei jedem Lauf mit seinem Agenten-Key ab und schreibt
daraus die lokale Datei; ist die API nicht erreichbar, pingt es unveraendert nach der
zuletzt geschriebenen Liste weiter.

Beim ersten Lauf laedt der Check seine bestehende hosts.conf hoch, damit die bereits
ueberwachten Server nicht von Hand nachgetragen werden muessen. Dass die API das nur
einmal annimmt (Marker in host_config_imports) und "noch nie konfiguriert" per 204 von
"im Frontend geleert" per 200 unterscheidet, ist noetig, damit ein Ping-Rechner mit alter
lokaler Datei die dort geloeschten Hosts nicht wieder anlegt.

Loeschen raeumt mit auf: Ein entfernter Host nimmt seine Kachel mit - beide Ids, denn der
status.txt-Import benennt ihn nach dem Hostnamen, die REST-Meldung nach dessen Slug.
Kacheln ohne zugehoerigen Host (Selbstmelder oder Reste laengst entfernter Hosts) stehen
im selben Dialog unter "Weitere Kacheln im System-Monitor" und sind dort einzeln
loeschbar - dafuer gibt es neu DELETE /api/v1/systems/{id}.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vNjbfr8M3HSfWMee92GTE
2026-09-01 13:14:11 +02:00

915 lines
18 KiB
CSS

/* Buero-Visualisierung (Phase 2, statische Szene). Bewusst ohne UI-Framework, siehe
Visualization.md Abschnitt 5. Farbschema hell/freundlich (F9: "Zeichentrick"-Anmutung). */
:root {
--color-floor: #eef2f7;
--color-wall: #cfe0f0;
--color-wall-side: #bcd3ea;
--color-door: #8fb8dd;
--color-board-frame: #94622f;
--color-note: #fde68a;
--color-note-border: #d9b34f;
--color-note-stack-badge: #d97706;
--color-monitor-frame: #2b2f38;
--color-monitor-screen: #10131a;
--color-monitor-placeholder: #1a2436;
--color-desk-table: #b98a54;
--color-desk-chair: #6b7280;
--color-desk-screen-idle: #3a3f4b;
--color-text: #1f2937;
--color-text-inverse: #f8fafc;
}
* {
box-sizing: border-box;
}
/* Die Szene soll immer vollstaendig sichtbar sein - auch im normalen Fenstermodus,
ohne Scrollen. Dazu spannt der body eine Flex-Spalte ueber die Viewport-Hoehe auf:
Header nimmt seine natuerliche Hoehe, #office-scene bekommt den Rest, und das SVG
skaliert sich per aspect-ratio in diesen Rest hinein (siehe unten). */
html {
height: 100%;
}
body {
margin: 0;
font-family: "Segoe UI", Verdana, Arial, sans-serif;
background: #dbe7f3;
color: var(--color-text);
/* height (nicht min-height): Nur eine definite Hoehe laesst die prozentualen
max-height-Angaben weiter unten aufloesen. dvh rechnet mobile Browserleisten
mit, vh ist der Fallback fuer aeltere Engines. */
height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
header.office-header {
flex: 0 0 auto;
padding: 12px 24px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
header.office-header h1 {
margin: 0;
font-size: 1.4rem;
}
/* Rein kosmetischer Countdown bis zum naechsten Codeberg-Sync (Issue #12), rechtsbuendig
anstelle des frueheren erklaerenden Fliesstexts. Tabular-nums verhindert Wackeln der
Breite, wenn sich die Ziffernzahl aendert (z.B. 9s -> 10s). */
.reload-countdown {
color: #475569;
font-size: 0.9rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Warnbanner, wenn die API nicht erreichbar ist und die Szene auf Demo-Daten zurueckfaellt
(siehe setApiBanner() in js/office-render.js). */
.api-banner {
flex: 0 0 auto;
padding: 10px 24px;
background: #dc2626;
color: #ffffff;
font-weight: 700;
text-align: center;
letter-spacing: 0.02em;
}
/* Muss nach der Regel oben stehen: ohne diese Zeile wuerde das hidden-Attribut vom
flex-Wert ueberstimmt und der Banner waere dauerhaft sichtbar. */
.api-banner[hidden] {
display: none;
}
/* Reine Vorwarnung (Anmeldung laeuft demnaechst ab, der Agent arbeitet noch): gelb statt rot,
damit das Rot dem tatsaechlichen Stillstand vorbehalten bleibt. Dunkle Schrift, weil Weiss
auf Gelb kaum zu lesen ist. */
.api-banner.api-banner-warning {
background: #facc15;
color: #422006;
}
#office-scene {
flex: 1 1 auto;
/* min-height: 0 hebt die Flex-Default-Mindesthoehe (min-content) auf - sonst kann
das Kind den Container nicht unterschreiten und die Szene laeuft aus dem Bild. */
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px 16px;
}
/* Das SVG soll sich an der jeweils knapperen Achse ausrichten. Welche das ist, haengt
vom Seitenverhaeltnis des Fensters ab - daher die beiden Media-Queries unten. Die
max-*-Grenzen sind das Sicherheitsnetz fuer den Grenzbereich (die Header-Hoehe geht
in das Verhaeltnis der Szenenflaeche ein, in die Media-Query aber nicht): Dort wird
die Elementbox notfalls leicht gestaucht - der Szeneninhalt bleibt dank
preserveAspectRatio unverzerrt und vollstaendig sichtbar. */
.office-svg {
max-width: 100%;
max-height: 100%;
aspect-ratio: 1920 / 1080;
display: block;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.25);
}
/* Fenster breiter als 16:9 -> die Hoehe ist der Engpass. */
@media (min-aspect-ratio: 16 / 9) {
.office-svg {
height: 100%;
width: auto;
}
}
/* Fenster schmaler als 16:9 -> die Breite ist der Engpass. */
@media (max-aspect-ratio: 16 / 9) {
.office-svg {
width: 100%;
height: auto;
}
}
@media (max-height: 640px) {
header.office-header {
padding: 8px 16px;
}
header.office-header h1 {
font-size: 1.1rem;
}
}
.floor {
fill: var(--color-floor);
}
.wall {
fill: var(--color-wall);
}
.wall-side {
fill: var(--color-wall-side);
}
.door {
fill: var(--color-door);
stroke: #5f86ab;
stroke-width: 3;
transition: fill 0.4s ease-in-out;
}
/* Phase 6 (F2): kurzes Aufleuchten, waehrend ein "user"-Tuerbesuch die Tuer passiert. */
.door-active {
fill: #d7ecff;
}
.wall-label {
fill: #f8fafc;
font-size: 22px;
font-weight: 600;
}
/* Kanban-Board */
.board-frame {
fill: #7a5230;
stroke: var(--color-board-frame);
stroke-width: 6;
}
.board-column-label {
fill: var(--color-text-inverse);
font-size: 16px;
font-weight: 600;
}
.board-column-divider {
stroke: rgba(255, 255, 255, 0.25);
stroke-width: 2;
}
.note {
fill: var(--color-note);
stroke: var(--color-note-border);
stroke-width: 2;
}
/* Zettel in der Farbe seines Projekts (Issue #27): die Fuellung kommt als Inline-Style aus
der Projektkonfiguration, die Kontur muss dann neutral sein - die gelbe Standardkontur
passt nur zum gelben Standardzettel. */
.note-colored {
stroke: rgba(15, 23, 42, 0.35);
}
.note-stack-badge {
fill: var(--color-note-stack-badge);
stroke: #92400e;
stroke-width: 2;
}
.note-stack-count {
fill: #ffffff;
font-size: 20px;
font-weight: 700;
}
/* Monitor-Wand */
.monitor-frame {
fill: var(--color-monitor-frame);
}
.monitor-screen {
fill: var(--color-monitor-screen);
}
.monitor-screen-placeholder {
fill: var(--color-monitor-placeholder);
}
.monitor-event-line {
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
/* Grundfarbe fuer Schweregrade ohne eigene Farbe (z.B. info) - ohne sie stuende der Text
schwarz auf dem dunklen Monitor. */
fill: #cbd5e1;
}
/* Kopfzeile eines Events (Zeit + System): tritt gegenueber der Meldung zurueck. */
.monitor-event-meta {
opacity: 0.75;
}
.severity-warning,
.severity-warning .monitor-event-line {
fill: #facc15;
}
.severity-error,
.severity-error .monitor-event-line {
fill: #f87171;
}
.system-tile-bg {
fill: #1e293b;
}
.system-tile-bar {
fill: #334155;
}
.system-tile-led {
fill: #64748b;
}
.status-ok .system-tile-led {
fill: #4ade80;
}
.status-error .system-tile-led {
fill: #f87171;
}
.system-tile-label {
fill: var(--color-text-inverse);
font-size: 12px;
}
.monitor-placeholder-text {
fill: #64748b;
font-size: 15px;
}
/* Schreibtische */
.desk-table {
fill: var(--color-desk-table);
stroke: #8a6238;
stroke-width: 3;
}
.desk-screen {
fill: var(--color-desk-screen-idle);
stroke: #1f2430;
stroke-width: 2;
}
.desk-screen.pose-working {
fill: #7dd3fc;
}
/* Blockierter Platz (Claude-Anmeldung abgelaufen): der Bildschirm traegt den Zustand mit,
damit er auch dann am Platz sichtbar bleibt, wenn der Avatar gerade nicht dort steht. */
.desk-screen.pose-auth_expired {
fill: #7f1d1d;
stroke: #f87171;
}
.desk-screen.pose-auth_expiring {
fill: #713f12;
stroke: #facc15;
}
.desk-chair {
fill: var(--color-desk-chair);
}
.person-head {
fill: #f3d9b1;
stroke: #d8b98a;
stroke-width: 1.5;
}
.person-body {
fill: var(--role-color, #94a3b8);
}
.desk-name {
fill: #334155;
font-size: 14px;
font-weight: 600;
}
.desk-name-empty {
fill: #94a3b8;
font-style: italic;
font-weight: 400;
}
/* Der Name eines Agenten am Schreibtisch steht am Tisch, nicht am Avatar (updateAgentsLayer()
blendet .avatar-name am Platz aus) - der Problemzustand faerbt deshalb hier. */
.desk-name.desk-name-problem-error {
fill: #b91c1c;
font-weight: 700;
}
.desk-name.desk-name-problem-warning {
fill: #a16207;
font-weight: 700;
}
.desk-deco-star {
fill: #fbbf24;
}
.desk-deco-flower circle {
fill: #f9a8d4;
}
.desk-deco-flower-center {
fill: #fde047 !important;
}
.desk-deco-check {
fill: none;
stroke: #22c55e;
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
}
.desk-deco-gear circle,
.desk-deco-gear rect {
fill: #a78bfa;
}
/* Heller Kern, damit der gefuellte Kreis als Zahnrad-Ring lesbar wird (wie beim Bluetenkern). */
.desk-deco-gear-center {
fill: #ede9fe !important;
}
.desk-empty .desk-chair {
opacity: 0.5;
}
/* Klickbare Bereiche (Phase 3/4: Board-Spalten + Monitore oeffnen ein Modal) */
.clickable {
cursor: pointer;
}
.board-column-hit {
fill: transparent;
pointer-events: all;
}
/* Trefferflaeche des PO-Platzes (Issue #27): macht den ganzen Platz klickbar, nicht nur die
gefuellten Moebelstuecke - ein Klick daneben landete sonst auf dem Boden. */
.po-desk-hit {
fill: transparent;
pointer-events: all;
}
/* Konfig-Zahnrad (Issue #27 am PO-Platz, Issue #70 am Systeme-Monitor): zeigt ueberhaupt
erst, dass sich hier etwas einstellen laesst. Aussehen ist ueberall gleich, nur der
Ausloeser des Hovers unterscheidet sich - beim PO der ganze Schreibtisch, beim Monitor
das Symbol selbst (der Monitor traegt schon eine eigene Klick-Aktion). */
.config-hint-bg {
fill: #ffffff;
stroke: #94a3b8;
stroke-width: 1.5;
opacity: 0.85;
}
.config-hint-tooth,
.config-hint-hub {
stroke: #475569;
stroke-width: 2;
stroke-linecap: round;
fill: none;
}
.po-desk:hover .config-hint-bg,
.system-config-hint:hover .config-hint-bg {
fill: #e0f2fe;
stroke: #0284c7;
opacity: 1;
}
.po-desk:hover .config-hint-tooth,
.po-desk:hover .config-hint-hub,
.system-config-hint:hover .config-hint-tooth,
.system-config-hint:hover .config-hint-hub {
stroke: #0284c7;
}
.system-config-hint-hit {
fill: transparent;
pointer-events: all;
}
/* Reload-Symbol neben der Board-Ueberschrift (Issue #51): stoesst ein komplettes
Neueinlesen des Kanban-Boards an, analog zum Zahnrad am PO-Platz. */
.board-reload-hint-bg {
fill: #ffffff;
stroke: #94a3b8;
stroke-width: 1.5;
opacity: 0.85;
}
.board-reload-hint-arrow {
fill: none;
stroke: #475569;
stroke-width: 2;
stroke-linecap: round;
}
.board-reload-hint-arrowhead {
fill: #475569;
}
.board-reload-hint-hit {
fill: transparent;
pointer-events: all;
}
.board-reload-hint:hover .board-reload-hint-bg {
fill: #e0f2fe;
stroke: #0284c7;
opacity: 1;
}
.board-reload-hint:hover .board-reload-hint-arrow {
stroke: #0284c7;
}
.board-reload-hint:hover .board-reload-hint-arrowhead {
fill: #0284c7;
}
.just-updated .board-frame,
.just-updated .monitor-frame {
animation: office-flash 0.9s ease-out;
}
@keyframes office-flash {
0% {
filter: drop-shadow(0 0 0 rgba(250, 204, 21, 0));
}
25% {
filter: drop-shadow(0 0 12px rgba(250, 204, 21, 0.9));
}
100% {
filter: drop-shadow(0 0 0 rgba(250, 204, 21, 0));
}
}
/* Kaffee-Ecke (Phase 5, Raum-Prop zwischen den Schreibtischreihen) */
.coffee-table {
fill: #8a6238;
stroke: #6b4a28;
stroke-width: 2;
}
.coffee-cup {
fill: #f8fafc;
stroke: #94a3b8;
stroke-width: 1.5;
}
.coffee-icon {
font-size: 20px;
}
/* Agenten-Avatare (Phase 5: bewegliche Schicht, siehe office-render.js) */
.agent-avatar {
transition: transform 1.1s ease-in-out;
}
.agent-avatar .person-head {
fill: #f3d9b1;
stroke: #d8b98a;
stroke-width: 1.5;
}
.agent-avatar .person-body {
fill: var(--role-color, #94a3b8);
}
.agent-avatar.pose-walking .person-leg {
fill: #475569;
transform-box: fill-box;
transform-origin: top center;
animation: office-leg-swing 0.6s ease-in-out infinite alternate;
}
.agent-avatar.pose-walking .person-leg-right {
animation-delay: 0.3s;
}
@keyframes office-leg-swing {
from {
transform: rotate(-18deg);
}
to {
transform: rotate(18deg);
}
}
/* Blockierter Agent (AGENT_PROBLEM_STATES): der Koerper verliert seine Rollenfarbe, damit die
Figur als "steht still" lesbar wird, ohne aus der Szene zu verschwinden. */
.agent-avatar.pose-auth_expired .person-body,
.agent-avatar.pose-auth_expiring .person-body {
fill: #94a3b8;
}
.person-prop-lock-body {
fill: #f87171;
stroke: #7f1d1d;
stroke-width: 1.5;
}
.person-prop-lock-shackle {
fill: none;
stroke: #f87171;
stroke-width: 3;
stroke-linecap: round;
}
.pose-auth_expiring .person-prop-lock-body {
fill: #facc15;
stroke: #713f12;
}
.pose-auth_expiring .person-prop-lock-shackle {
stroke: #facc15;
}
/* Nur der harte Ausfall pulsiert - eine pulsierende Vorwarnung waere auf einem Dauermonitor
Rauschen, das man nach einer Stunde nicht mehr sieht. */
.agent-avatar.pose-auth_expired .person-prop-lock {
animation: office-lock-pulse 1.4s ease-in-out infinite;
}
@keyframes office-lock-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.35;
}
}
.person-prop-cup {
fill: #f8fafc;
stroke: #94a3b8;
}
.person-prop-newspaper {
fill: #f8fafc;
stroke: #94a3b8;
stroke-width: 1;
}
.person-prop-speech {
fill: #f8fafc;
stroke: #94a3b8;
stroke-width: 1;
}
.avatar-name {
fill: #334155;
font-size: 13px;
font-weight: 600;
text-anchor: middle;
}
/* KPI-Sparklines (Phase 4, F10: kein Chart-Lib, eigenes SVG-Zeichnen) */
.kpi-tile-label {
fill: var(--color-text-inverse);
font-size: 13px;
font-weight: 600;
}
/* Kombiniert notiert, weil .kpi-tile-label weiter unten steht als .severity-* und die Farbe
sonst bei gleicher Spezifitaet ueber die Reihenfolge zurueckgewinnt. */
.kpi-tile-label.severity-error {
fill: #f87171;
}
.kpi-tile-label.severity-warning {
fill: #facc15;
}
.kpi-sparkline-bg {
fill: rgba(255, 255, 255, 0.04);
}
.kpi-sparkline-line {
fill: none;
stroke: #7dd3fc;
stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
}
/* Modal (Phase 3/4: Board-/Monitor-Detailansicht, Visualization.md 4.5) */
.office-modal {
border: none;
border-radius: 12px;
padding: 0;
width: min(640px, 92vw);
max-height: 80vh;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.4);
}
.office-modal::backdrop {
background: rgba(15, 23, 42, 0.55);
}
.office-modal-form {
display: flex;
flex-direction: column;
max-height: 80vh;
}
.office-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e2e8f0;
}
.office-modal-title {
margin: 0;
font-size: 1.1rem;
}
.office-modal-close {
border: none;
background: transparent;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
color: #64748b;
}
.office-modal-body {
padding: 16px 20px;
overflow-y: auto;
}
.office-modal-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.office-modal-item {
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 10px 12px;
}
.office-modal-item p {
margin: 6px 0 0;
color: #475569;
font-size: 0.9rem;
}
.office-modal-item-heading {
display: flex;
align-items: center;
gap: 8px;
}
.office-modal-badge {
font-size: 0.75rem;
padding: 2px 8px;
border-radius: 999px;
background: #e2e8f0;
color: #1f2937;
}
.office-modal-item.severity-error {
border-color: #fca5a5;
background: #fef2f2;
}
.office-modal-item.severity-warning {
border-color: #fde68a;
background: #fffbeb;
}
.office-modal-item.status-error {
border-color: #fca5a5;
background: #fef2f2;
}
.office-modal-empty {
color: #64748b;
font-style: italic;
}
.kpi-modal-sparkline {
width: 100%;
height: 80px;
margin-top: 8px;
background: #10131a;
border-radius: 6px;
}
/* Projekte-Modal (Issue #27: Klick auf den PO -> Projekte des Codeberg-Sync verwalten) */
.office-modal-error {
margin: 0 0 10px;
padding: 8px 12px;
border-radius: 6px;
background: #fef2f2;
border: 1px solid #fca5a5;
color: #991b1b;
font-size: 0.9rem;
}
.repo-config-item {
display: flex;
align-items: center;
gap: 10px;
}
.repo-config-swatch {
flex: none;
width: 16px;
height: 16px;
border-radius: 4px;
border: 1px solid rgba(15, 23, 42, 0.15);
}
.repo-config-slug {
flex: 1;
font-weight: 600;
color: #1f2937;
}
.repo-config-actions {
display: flex;
gap: 4px;
flex: none;
}
.repo-config-form {
flex-wrap: wrap;
}
.repo-config-form-fields {
display: flex;
flex: 1;
gap: 8px;
min-width: 0;
}
.repo-config-form-fields input[type="text"] {
flex: 1;
min-width: 0;
padding: 6px 8px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 0.9rem;
}
.repo-config-form-fields input[type="color"] {
flex: none;
width: 36px;
height: 32px;
padding: 2px;
border: 1px solid #cbd5e1;
border-radius: 6px;
cursor: pointer;
}
.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: #475569;
cursor: pointer;
}
.icon-button:hover {
background: #f1f5f9;
border-color: #e2e8f0;
color: #1f2937;
}
.icon-button-svg {
width: 18px;
height: 18px;
}
.repo-config-add-button {
margin-top: 8px;
color: #16a34a;
}
.repo-config-add-button:hover {
background: #f0fdf4;
border-color: #bbf7d0;
color: #15803d;
}
/* Issue #53: Ampel-Symbole (Schreibrechte/Webhook) rechts neben den Bearbeiten-/
Loeschen-Symbolen einer Projektzeile - Tooltip (title) traegt den Pruefgrund. */
.repo-config-status {
display: flex;
gap: 2px;
flex: none;
}
.repo-config-status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: help;
}
.repo-config-status-icon.is-ok {
color: #16a34a;
}
.repo-config-status-icon.is-fail {
color: #dc2626;
}
/* Hosts-Modal (Issue #70) - teilt sich Zeilen-, Formular- und Symbolstile mit dem
Projekte-Modal; eigene Regeln braucht nur, was es dort nicht gibt. */
.host-config-subheading {
margin: 18px 0 4px;
font-size: 0.95rem;
color: #1f2937;
}
.host-config-agent {
flex: none;
padding: 2px 8px;
border-radius: 999px;
background: #f1f5f9;
color: #475569;
font-size: 0.8rem;
}