monitor/html/css/office.css
fds2610-bot d5f7ac9f92 Klick auf den PO oeffnet den Projekte-Dialog zuverlaessig
Der Konfig-Dialog liess sich in der Praxis kaum oeffnen (Fehlerbericht in
Issue #27). Drei Ursachen, alle reproduziert:

- Ein Klick auf den PO selbst tat gar nichts: der Klick-Handler hing nur am
  Schreibtisch, die Figur liegt aber in der Avatar-Schicht darueber. Genau sie
  meint das Ticket ("bei Klick auf den PO"). Der Avatar ist jetzt klickbar,
  auch waehrend er am Board steht.
- Klicks auf den Platz trafen nur, wenn sie zufaellig auf einem gefuellten
  Moebelstueck landeten - eine <g> faengt in ihren Luecken keine Klicks. Der
  Platz hat jetzt eine eigene Trefferflaeche, wie die Board-Spalten.
- Dass hinter dem PO ueberhaupt etwas steckt, war nicht zu sehen. Ein kleines
  Zahnrad auf seinem Tisch (mit Tooltip) zeigt es an.

Ausserdem oeffnet der Dialog jetzt auch bei nicht erreichbarer API - er meldet
dann selbst, dass er die Projekte nicht laden konnte, statt tot zu wirken.

Und genau dieser Fall trat leicht ein: der eingebaute PHP-Server beantwortet
Anfragen nacheinander, eine offene SSE-Verbindung blockiert damit alles andere
(gemessen 16s fuer ein GET /api/v1/tickets, ab dem zweiten offenen Monitor
dauerhaft). Das Frontend faellt dann auf Demo-Daten zurueck. Mit
PHP_CLI_SERVER_WORKERS=4 sind es 0,006s; PRAGMA busy_timeout deckt die
Schreibzugriffe ab, die sich jetzt begegnen koennen.

ApplicationTest::testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey war
seit dem Seed in 003_repo_configs.sql rot (legte fds2610/monitor ein zweites
Mal an) - der Test nutzt jetzt ein nicht geseedetes Repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 22:09:31 +02:00

709 lines
13 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;
}
#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;
}
.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;
}
.severity-warning {
fill: #facc15;
}
.severity-error {
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;
}
.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;
}
.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-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;
}
/* Zahnrad am PO-Platz (Issue #27): zeigt ueberhaupt erst, dass sich hier die
Projektkonfiguration oeffnen laesst. Beim Hover ueber den Platz tritt es hervor. */
.po-config-hint-bg {
fill: #ffffff;
stroke: #94a3b8;
stroke-width: 1.5;
opacity: 0.85;
}
.po-config-hint-tooth,
.po-config-hint-hub {
stroke: #475569;
stroke-width: 2;
stroke-linecap: round;
fill: none;
}
.po-desk:hover .po-config-hint-bg {
fill: #e0f2fe;
stroke: #0284c7;
opacity: 1;
}
.po-desk:hover .po-config-hint-tooth,
.po-desk:hover .po-config-hint-hub {
stroke: #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);
}
}
.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;
}
.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;
}