Design: Pflicht-Fixes gegen helle Schrift und dunkle Flaechen, Diagrammfarben, Chat-Variablen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 06:02:54 +00:00
1 parent 47a26fb9b2
commit f911dbcb03
17 files changed
+143 -103

No files matched your search

@@ -176,3 +176,22 @@ Die übrigen 58 hatten schon vor P1 keine Regel:
- die Bootstrap-Namen aus `WorksheetList.jsx` (wirkungslos): `alert`, `alert-info`, `align-items-center`, `border-0`, `col-md-6`, `d-flex`, `fs-5`, `fs-6`, `justify-content-between`, `justify-content-md-end`, `mb-3`, `mb-4`, `me-1`, `me-2`, `ms-2`, `ms-3`, `mx-2`, `overflow-hidden`, `p-3`, `px-3`, `py-2`, `py-3`, `rounded-3`, `shadow-sm`, `spinner-border`, `text-dark`, `text-md-end`, `text-muted`, `visually-hidden`, `timeline`, `timeline-item`;
- die Namen aus dem Plan: `p-4`, `btn-sm`, `status-filter`, `supporters-editor` (und `timeline` oben);
- reine Haken ohne Styling: `credits-view`, `fin-abo-amount`, `fin-abo-main`, `fin-abo-next`, `fin-abo-product`, `fin-assign`, `fin-days`, `fin-inv-abo`, `fin-memo`, `fin-tax-card`, `fin-tax-note`, `fin-todo-main`, `fin-total-value`, `fin-tx-out`, `fin-wrap`, `legal-doc`, `offer-form`, `sp-ampel`, `sp-apps`, `sp-cleanup-body`, `status-filter-actions`, `worksheet-list`, `worksheet-supporters`.
## Bewusst offen (P1 Task 9)
Stand nach Task 9: `node "$W/farben.mjs" "$WT"` meldet **0 Treffer** in allen vier Gründen (`kontrast<3`, `dunkel-auf-akzent`, `weiss`, `verlauf`). Vorher waren es 41, 1, 18 und 11 (zusammen 71). Was danach noch an Farbliteralen im Quelltext steht, ist hier mit Zielpaket und gemessenem Kontrast festgehalten; keine Zeile liegt unter 3:1.
| Stelle | Was | Kontrast (gemessen) | Zielpaket |
|---|---|---|---|
| `global.css:269` (`.select`) | Pfeil als zwei `linear-gradient` mit hartem Stopp (`transparent 50%`), Wert aus dem Prototyp; `farben.mjs` führt ihn als „ausgenommen“ (`--ohne-ausnahmen` zeigt ihn) | `--text-2` auf Weiß 5,76:1 | bleibt (Spec) |
| `global.css:1057` (KOMPAT, `select.form-control`) | derselbe Pfeil für das Altfeld | 5,76:1 | P10 (KOMPAT entfällt, `FormField` und die Altfelder gehen in P7 auf `.select`) |
| `global.css:669` `.legal-version-current` (Tönung `rgba(16,185,129,.08)`), `:680` `.legal-tone-red td` (`rgba(239,68,68,.10)`), `:682` `.legal-tone-orange td` (`rgba(245,158,11,.10)`) | Alpha-Tönungen als Zeilenhintergrund | Text `--text` auf den Tönungen 14,3 bis 15,1:1; `.legal-deadline` rot (`--danger`) 5,43:1, orange (`--orange-fg`) 6,43:1 | P8 (Rechtliches, Tönungen werden Badge-Töne) |
| `global.css:964` `.fin-confirm` (Tönung `rgba(245,158,11,.1)`) | Alpha-Tönung als Kastenhintergrund | Text `--text` auf der Tönung 15,1:1 | P7 (Finanzen) |
| `src/components/serverpuls/chartColors.js` | Canvas-Farben, die einzige erlaubte Ausnahme im Quelltext (Spec) | Linie 18,88:1, Achse 5,76:1, Spitze 3,10:1; Raster und Teilstriche sind Hilfslinien ohne Text | bleibt (Spec) |
| `src/components/Logo.jsx` (`fill="#111"`, `fill="#fff"`) | SVG-Marke mit zwei festen Farben | Weiß auf Schwarz 18,88:1 | bleibt (Marke) |
Außerhalb des Scans (er prüft nur CSS und JSX) und **nicht** unter „bewusst offen“, weil der Kontrast unter 3:1 liegt:
- `src/lib/previewEnv.js`, `DOT_COLORS` und `dotColor()`: Status-Punkte mit den Altfarben `#10b981` (2,54:1), `#f59e0b` (2,15:1), `#ef4444` (3,76:1) und `#a0aec0` (2,26:1) auf Weiß. Die Punkte sind höchstens 10 × 10 px groß und stehen immer neben dem Statustext (`ServicesPage`, `PreviewBlock`); der Browser-Check `dark-surface` ignoriert seit diesem Task Elemente bis 12 × 12 px. `tests/unit/previewEnv.test.mjs` hält die Hex-Werte fest, deshalb bleibt die Datei hier unverändert. Zielpaket: P5 (Projekte und Services): `dotColor()` liefert `var(--green-fg)`, `var(--orange-fg)`, `var(--red-fg)`, `var(--text-3)`, dazu der Test.
Nicht prüfbar im Werkzeug: Der Eingabebaustein des Chats kommt aus dem Kundenportal und ist dort eine leere Datei (`loadChatComposer`). Die Variablen `--chat-*` und die Meldungsfarben (`.chat-composer-note`, `.chat-file-size`, `.chat-audio-time`) sind laut Vorgabe gesetzt; ihr Laufzeitbild muss nach dem Livegang einmal im Chat angesehen werden (Zielpaket P6).
+1 -1
View File
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { fetchAnalyse, setAnalyse } from '../lib/analyseApi'
import { analyseStatus, textformLabel, canSubmitTextform, channelLabel, legalLinks, chatTemplate, privacyHint } from '../lib/analyseView'
const COLORS = { green: '#10b981', red: '#ef4444', grey: '#a0aec0' }
const COLORS = { green: 'var(--green-fg)', red: 'var(--red-fg)', grey: 'var(--text-2)' }
// Kompakte Zeile "Website-Analyse" je Projekt (lädt beim Einbinden; mit key=Projekt-ID verwenden)
export default function AnalyseRow({ projectId }) {
+12 -12
View File
@@ -10,21 +10,21 @@ import { needsPreviewButton, assignSummary } from '../lib/projectAssign'
import { updateWebsiteProject, backfillProjectPushes } from '../lib/projectAdminApi'
const STATUS_COLORS = {
ready: '#10b981',
live: '#10b981',
online: '#10b981',
provisioning: '#f59e0b',
pending: '#f59e0b',
building: '#3b82f6',
error: '#ef4444',
failed: '#ef4444',
ready: 'var(--green-fg)',
live: 'var(--green-fg)',
online: 'var(--green-fg)',
provisioning: 'var(--orange-fg)',
pending: 'var(--orange-fg)',
building: 'var(--blue-fg)',
error: 'var(--red-fg)',
failed: 'var(--red-fg)',
}
function StatusBadge({ label, value }) {
if (!value) return null
const color = STATUS_COLORS[String(value).toLowerCase()] || '#a0aec0'
const color = STATUS_COLORS[String(value).toLowerCase()] || 'var(--text-2)'
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#cbd5e0', marginRight: 12 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-2)', marginRight: 12 }}>
{label}:
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
@@ -79,7 +79,7 @@ export default function AssignedProjectCard({ project, onUnassign, showActions =
}
return (
<div style={{ background: 'rgba(30,41,59,0.5)', border: '1px solid rgba(59,130,246,0.25)', borderRadius: 10, padding: '12px 14px', marginBottom: 12 }}>
<div style={{ background: 'var(--bg-subtle)', border: '1px solid var(--line)', borderRadius: 10, padding: '12px 14px', marginBottom: 12 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start', justifyContent: 'space-between' }}>
<div style={{ minWidth: 0 }}>
<strong>{project.projectName}</strong>
@@ -99,7 +99,7 @@ export default function AssignedProjectCard({ project, onUnassign, showActions =
title="Vom Ticket lösen"
aria-label="Vom Ticket lösen"
onClick={() => onUnassign(project.$id)}
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, borderRadius: 6, background: 'rgba(239, 68, 68, 0.12)', border: '1px solid rgba(239, 68, 68, 0.35)', color: '#f87171', cursor: 'pointer' }}
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, borderRadius: 6, background: 'var(--red-bg)', border: '1px solid var(--line)', color: 'var(--red-fg)', cursor: 'pointer' }}
>
<FaLinkSlash size={16} />
</button>
+2 -2
View File
@@ -191,7 +191,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
))}
</select>
{isAcquisition && (
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
"Zugesagt" macht den Lead automatisch zum festen Kunden.
</small>
)}
@@ -259,7 +259,7 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
onChange={(value) => handleChange('totalTime', value)}
disabled={formData.isComment}
/>
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
{autoCalculate && formData.startTime && formData.endTime
? '✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
: autoCalculate ? 'Beginn und Ende eintragen oder Zeit wählen' : 'Von Hand gewählt'}
+3 -3
View File
@@ -17,9 +17,9 @@ export default function GiteaLinkButton({ href, title = 'In Gitea oeffnen', size
width: 32,
height: 32,
borderRadius: 6,
background: 'rgba(99, 102, 241, 0.15)',
border: '1px solid rgba(99, 102, 241, 0.35)',
color: '#818cf8',
background: 'var(--violet-bg)',
border: '1px solid var(--line)',
color: 'var(--violet-fg)',
textDecoration: 'none',
flexShrink: 0,
}}
+5 -5
View File
@@ -208,13 +208,13 @@ export default function PreviewBlock({ projectId }) {
<span style={{ overflowWrap: 'anywhere' }}>{status.text}</span>
</div>
{data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && (
<div style={{ color: '#f87171', ...small, marginTop: 4, overflowWrap: 'anywhere' }}>{data.error}</div>
<div style={{ color: 'var(--red-fg)', ...small, marginTop: 4, overflowWrap: 'anywhere' }}>{data.error}</div>
)}
</>
)}
{inactive && <div className="text-grey" style={{ ...small, marginTop: 8 }}>{NOT_ACTIVE}.</div>}
{error && <div style={{ color: '#f87171', ...small, marginTop: 8, overflowWrap: 'anywhere' }}>{error}</div>}
{error && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 8, overflowWrap: 'anywhere' }}>{error}</div>}
{data && !inactive && (
<>
@@ -251,7 +251,7 @@ export default function PreviewBlock({ projectId }) {
{logs.map((l) => <option key={l.runFile} value={l.runFile}>{logDate(l.at)}</option>)}
</select>
{logFile && (
<pre style={{ marginTop: 6, maxHeight: 260, overflow: 'auto', background: 'rgba(0,0,0,0.35)', padding: 8, borderRadius: 6, fontFamily: 'monospace', fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
<pre style={{ marginTop: 6, maxHeight: 260, overflow: 'auto', background: 'var(--bg-subtle)', padding: 8, borderRadius: 6, fontFamily: 'monospace', fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{logText || '…'}
</pre>
)}
@@ -263,7 +263,7 @@ export default function PreviewBlock({ projectId }) {
<div style={{ marginTop: 6 }}>
{names.length === 0 && <div className="text-grey" style={small}>Keine Variablen.</div>}
{names.map((n) => (
<div key={n} style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: '4px 0', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
<div key={n} style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: '4px 0', borderBottom: '1px solid var(--line)' }}>
<code style={{ flex: '1 1 140px', overflowWrap: 'anywhere' }}>{n}</code>
<span className="text-grey">•••</span>
{editName === n ? (
@@ -282,7 +282,7 @@ export default function PreviewBlock({ projectId }) {
<input type="password" autoComplete="off" className="form-control" style={{ flex: '1 1 140px', minWidth: 0 }} placeholder="Wert" onKeyDown={enter(handleAdd)} value={newValue} onChange={(e) => setNewValue(e.target.value)} />
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleAdd}>Hinzufügen</button>
</div>
{envError && <div style={{ color: '#f87171', ...small, marginTop: 4 }}>{envError}</div>}
{envError && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 4 }}>{envError}</div>}
{dirty && (
<div style={{ marginTop: 8 }}>
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleSaveEnv}>Variablen speichern</button>
+3 -3
View File
@@ -30,9 +30,9 @@ export default function PreviewLinkButton({ href, subdomain, title = 'Preview oe
width: 32,
height: 32,
borderRadius: 6,
background: 'rgba(16, 185, 129, 0.15)',
border: '1px solid rgba(16, 185, 129, 0.35)',
color: '#34d399',
background: 'var(--green-bg)',
border: '1px solid var(--line)',
color: 'var(--green-fg)',
textDecoration: 'none',
flexShrink: 0,
}}
+34 -33
View File
@@ -4,21 +4,22 @@ import { serviceTypeLabel, needsTimeEntry, worksheetTone } from '../lib/ticketFo
import { statusLabel } from '../lib/ticketStatus'
import { parseSupporters, supportMinutes, firstName } from '../lib/support'
// Kopffarbe je Zustand (worksheetTone): orange = Git-Zeit fehlt, gruen = Git-Zeit eingetragen
// Randfarbe je Zustand (worksheetTone): orange = Git-Zeit fehlt, gruen = Git-Zeit eingetragen. Der Kopf ist fuer alle Zustaende
// grau (Schrift --text auf --bg-subtle); P2 baut die Liste um.
const TONES = {
'git-open': { border: '#f97316', base: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)', hover: 'linear-gradient(135deg, #ea580c 0%, #c2410c 100%)' },
'git-done': { border: '#10b981', base: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', hover: 'linear-gradient(135deg, #059669 0%, #047857 100%)' },
comment: { border: '#3b82f6', base: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', hover: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)' },
work: { border: '#4a5568', base: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)', hover: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)' },
'git-open': { border: 'var(--orange-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
'git-done': { border: 'var(--green-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
comment: { border: 'var(--blue-fg)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
work: { border: 'var(--text-2)', base: 'var(--bg-subtle)', hover: 'var(--bg-hover)' },
}
// Hinweise im farbigen Kopf: mit der grauen Badge-Schrift waeren sie kaum lesbar
const headerBadgeStyle = { background: 'rgba(255,255,255,0.3)', color: 'white', marginLeft: '8px' }
// Hinweise im Kopf: graue Badge-Toene
const headerBadgeStyle = { background: 'var(--grey-bg)', color: 'var(--grey-fg)', marginLeft: '8px' }
const headerButtonStyle = {
background: 'rgba(255,255,255,0.25)',
color: 'white',
border: '1px solid rgba(255,255,255,0.4)',
background: 'var(--bg)',
color: 'var(--text)',
border: '1px solid var(--line-strong)',
borderRadius: '6px',
padding: '2px 10px',
fontSize: '0.8rem',
@@ -88,10 +89,10 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}} onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
e.currentTarget.style.boxShadow = 'var(--shadow-btn)'
}} onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
e.currentTarget.style.boxShadow = 'none'
}}>
{/* Header - Immer sichtbar, klickbar */}
<div
@@ -99,7 +100,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
onClick={() => toggleWorksheet(ws.wsid)}
style={{
background: colors.base,
color: 'white',
color: 'var(--text)',
border: 'none',
cursor: 'pointer',
userSelect: 'none',
@@ -141,7 +142,7 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
</span>
)}
{ws.countsToPlan && !ws.isComment && (
<span className="badge ms-2" title="Wird vom Stundenguthaben des Wartungs-Abos abgezogen" style={{ ...headerBadgeStyle, background: '#9d3dfe' }}>
<span className="badge ms-2" title="Wird vom Stundenguthaben des Wartungs-Abos abgezogen" style={{ ...headerBadgeStyle, background: 'var(--violet-bg)', color: 'var(--violet-fg)' }}>
Abo-Stunden
</span>
)}
@@ -160,8 +161,8 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
</div>
)}
<span className="badge" style={{
background: 'rgba(255,255,255,0.2)',
color: 'white',
background: 'var(--grey-bg)',
color: 'var(--grey-fg)',
fontSize: '0.8rem'
}}>{serviceTypeLabel(ws.serviceType)}</span>
</div>
@@ -207,9 +208,9 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
className="btn btn-sm"
title={ws.countsToPlan ? 'Nicht mehr vom Abo-Stundenkonto abziehen' : 'Vom Abo-Stundenkonto des Kunden abziehen'}
style={{
background: ws.countsToPlan ? '#9d3dfe' : 'rgba(255,255,255,0.15)',
color: 'white',
border: '1px solid rgba(255,255,255,0.4)',
background: ws.countsToPlan ? 'var(--violet-bg)' : 'var(--bg)',
color: ws.countsToPlan ? 'var(--violet-fg)' : 'var(--text)',
border: '1px solid var(--line-strong)',
borderRadius: '6px',
padding: '2px 10px',
fontSize: '0.8rem',
@@ -241,12 +242,12 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
<div className="row mb-3">
<div className="col-md-6">
<div className="d-flex align-items-center">
<FaUser className="me-2" style={{ color: '#10b981' }} />
<FaUser className="me-2" style={{ color: 'var(--green-fg)' }} />
<strong>{ws.employeeName}</strong>
{ws.employeeShort && (
<span className="badge ms-2" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white'
background: 'var(--green-bg)',
color: 'var(--green-fg)'
}}>{ws.employeeShort}</span>
)}
</div>
@@ -254,8 +255,8 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
<div className="col-md-6 text-md-end">
{!ws.isComment && (
<div className="d-flex align-items-center justify-content-md-end">
<FaClock className="me-2" style={{ color: '#10b981' }} />
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
<FaClock className="me-2" style={{ color: 'var(--green-fg)' }} />
<strong className="fs-5" style={{ color: 'var(--green-fg)' }}>{formatTime(ws.totalTime)}</strong>
</div>
)}
</div>
@@ -275,8 +276,8 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
{/* Service Type */}
<div className="mb-3">
<span className="badge px-3 py-2" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
background: 'var(--grey-bg)',
color: 'var(--grey-fg)',
fontSize: '0.85rem'
}}>{serviceTypeLabel(ws.serviceType)}</span>
</div>
@@ -284,20 +285,20 @@ export default function WorksheetList({ worksheets, totalTime, loading, onToggle
{/* Status-Änderung */}
{ws.oldStatus !== ws.newStatus && (
<div className="mb-3 p-3 rounded-3" style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
background: 'var(--bg-subtle)'
}}>
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
<FaExchangeAlt className="me-2" style={{ color: 'var(--green-fg)' }} />
<span className="text-muted">Status:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{statusLabel(ws.oldStatus)}</span>
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{statusLabel(ws.newStatus)}</span>
<span className="badge" style={{ background: 'var(--grey-bg)', color: 'var(--grey-fg)' }}>{statusLabel(ws.oldStatus)}</span>
<span className="mx-2" style={{ color: 'var(--green-fg)' }}>→</span>
<span className="badge" style={{ background: 'var(--green-bg)', color: 'var(--green-fg)' }}>{statusLabel(ws.newStatus)}</span>
</div>
)}
{/* Details */}
<div className="mt-3 p-3 rounded-3" style={{
background: 'rgba(16, 185, 129, 0.05)',
border: '1px solid rgba(16, 185, 129, 0.1)'
background: 'var(--bg-subtle)',
border: '1px solid var(--line)'
}}>
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
{ws.details}
+1 -1
View File
@@ -140,7 +140,7 @@ export default function WorksheetTimesModal({ worksheet, currentUserId, onClose,
value={totalTime}
onChange={(value) => setManualTotal(value)}
/>
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
<small style={{ color: 'var(--text-2)', fontSize: '12px' }}>
{manualTotal === null && calculatedTotal !== null
? '✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten'
: 'Von Hand gewählt'}
+8 -7
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'
import uPlot from 'uplot'
import { formatAxisTick, formatLegendTime } from '../../lib/serverpulsFormat'
import { CHART } from './chartColors'
import 'uplot/dist/uPlot.min.css'
/**
@@ -25,15 +26,15 @@ export default function Chart({ data, series, height = 280, yFmt, yRange }) {
scales: { y: { range: yr || ((u, min, max) => [Math.min(0, min), max === min ? min + 1 : max + (max - min) * 0.08]) } },
axes: [
{
stroke: '#9aa7b4',
grid: { stroke: 'rgba(255,255,255,0.06)' },
ticks: { stroke: 'rgba(255,255,255,0.1)' },
stroke: CHART.axis,
grid: { show: false },
ticks: { stroke: CHART.tick },
values: (u, splits, axisIdx, foundSpace, foundIncr) => splits.map((t) => formatAxisTick(t, foundIncr)),
},
{
stroke: '#9aa7b4',
grid: { stroke: 'rgba(255,255,255,0.06)' },
ticks: { stroke: 'rgba(255,255,255,0.1)' },
stroke: CHART.axis,
grid: { stroke: CHART.grid },
ticks: { stroke: CHART.tick },
size: 70,
values: (u, vals) => vals.map(fmtVal),
},
@@ -42,7 +43,7 @@ export default function Chart({ data, series, height = 280, yFmt, yRange }) {
{ label: 'Zeit', value: (u, v) => formatLegendTime(v) },
...s.map((x) => ({
label: x.label,
stroke: x.color || '#10b981',
stroke: x.color || CHART.line,
width: 2,
spanGaps: false,
points: { show: false },
+11
View File
@@ -0,0 +1,11 @@
// Canvas-Farben fuer uPlot (Serverpuls). Canvas versteht kein var(--...), deshalb stehen die Werte hier als Literale;
// sie entsprechen den Tokens: line = --accent, peak = --text-3, axis = --text-2, grid = --line, tick = --line-strong.
// Einzige Stelle im Quelltext, an der Farbliterale ausserhalb von CSS erlaubt sind.
export const CHART = {
line: '#111111',
fill: 'rgba(17,17,17,.06)',
peak: '#8a93a3',
axis: '#5e6678',
grid: '#ebeef1',
tick: '#d8dde3',
}
+12 -12
View File
@@ -142,15 +142,15 @@
}
.admin-icon-button:hover {
border-color: rgba(16, 185, 129, 0.5);
border-color: var(--accent-ring);
color: var(--text);
background: rgba(16, 185, 129, 0.07);
background: var(--accent-soft);
}
.admin-icon-button.danger:hover {
border-color: rgba(239, 68, 68, 0.55);
color: #fca5a5;
background: rgba(239, 68, 68, 0.08);
border-color: var(--danger);
color: var(--red-fg);
background: var(--red-bg);
}
.admin-primary-button {
@@ -160,17 +160,17 @@
justify-content: center;
gap: 8px;
padding: 9px 15px;
border: 1px solid #34d399;
border: 1px solid var(--accent);
border-radius: 6px;
background: var(--accent);
color: #05251c;
color: #fff;
font: inherit;
font-size: 13px;
font-weight: 750;
cursor: pointer;
}
.admin-primary-button:hover { background: #34d399; }
.admin-primary-button:hover { background: var(--accent-hover); }
.admin-primary-button:disabled { opacity: 0.55; cursor: wait; }
.admin-split-layout {
@@ -259,14 +259,14 @@
.admin-table td {
padding: 12px 13px;
border-bottom: 1px solid rgba(255, 255, 255, 0.065);
border-bottom: 1px solid var(--line);
color: var(--text);
font-size: 13px;
vertical-align: middle;
}
.admin-table tbody tr:last-child td { border-bottom: 0; }
.admin-table tbody tr:hover td { background: rgba(255, 255, 255, 0.018); }
.admin-table tbody tr:hover td { background: var(--bg-subtle); }
.admin-table td > strong,
.admin-table td > span { display: block; }
.admin-table td > span { margin-top: 3px; color: var(--text-muted); font-size: 12px; }
@@ -350,8 +350,8 @@
align-items: center;
margin-bottom: 16px;
padding: 13px 15px;
border: 1px solid rgba(16, 185, 129, 0.42);
background: rgba(16, 185, 129, 0.065);
border: 1px solid var(--line-strong);
background: var(--bg-subtle);
}
.admin-credential-result strong,
+6 -5
View File
@@ -7,6 +7,7 @@ import {
ageLabel, formatBytes, formatNumber, formatPct, formatSince, usedPct,
} from '../lib/serverpulsFormat'
import Chart from '../components/serverpuls/Chart'
import { CHART } from '../components/serverpuls/chartColors'
import Sparkline from '../components/serverpuls/Sparkline'
import AppsTable from '../components/serverpuls/AppsTable'
import '../styles/serverpuls.css'
@@ -220,10 +221,10 @@ export default function ServerpulsPage() {
const avg = pts.map((p) => p.v)
const withMax = chart.res !== 'raw' && ['cpu', 'load', 'zombies'].includes(chart.metric)
const data = [xs, avg]
const series = [{ label: m.label, color: '#10b981', fill: 'rgba(16,185,129,0.10)' }]
const series = [{ label: m.label, color: CHART.line, fill: CHART.fill }]
if (withMax) {
data.push(pts.map((p) => p.max ?? null))
series.push({ label: `${m.label} (Spitze)`, color: '#f59e0b', dash: [4, 4] })
series.push({ label: `${m.label} (Spitze)`, color: CHART.peak, dash: [4, 4] })
}
return { data, series }
}, [chart, m.label])
@@ -232,9 +233,9 @@ export default function ServerpulsPage() {
if (!detail) return null
const days = (detail.size || []).map((s) => Math.round(new Date(`${s.day}T12:00:00Z`).getTime() / 1000))
return {
size: { data: [days, (detail.size || []).map((s) => s.bytes)], series: [{ label: 'Größe', color: '#3b82f6', fill: 'rgba(59,130,246,0.10)' }] },
mem: { data: [(detail.mem || []).map((p) => Math.round(p.ts / 1000)), (detail.mem || []).map((p) => p.v)], series: [{ label: 'RAM', color: '#10b981' }] },
cpu: { data: [(detail.cpu || []).map((p) => Math.round(p.ts / 1000)), (detail.cpu || []).map((p) => p.v)], series: [{ label: 'CPU', color: '#f59e0b' }] },
size: { data: [days, (detail.size || []).map((s) => s.bytes)], series: [{ label: 'Größe', color: CHART.line, fill: CHART.fill }] },
mem: { data: [(detail.mem || []).map((p) => Math.round(p.ts / 1000)), (detail.mem || []).map((p) => p.v)], series: [{ label: 'RAM', color: CHART.line }] },
cpu: { data: [(detail.cpu || []).map((p) => Math.round(p.ts / 1000)), (detail.cpu || []).map((p) => p.v)], series: [{ label: 'CPU', color: CHART.line }] },
}
}, [detail])
+4 -4
View File
@@ -9,12 +9,12 @@
.area-row.head { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.area-row.active { background: var(--accent-soft); }
.area-muted { color: var(--text-muted); font-size: 13px; }
.area-danger { color: #f87171; font-size: 13px; }
.area-warn { color: #fbbf24; font-size: 13px; }
.area-danger { color: var(--red-fg); font-size: 13px; }
.area-warn { color: var(--orange-fg); font-size: 13px; }
.area-summary { padding: 12px 16px; border-radius: var(--r-sm); background: var(--surface-2); margin-bottom: var(--sp-3); }
.area-summary.warn { background: rgba(245, 158, 11, 0.16); color: #fbbf24; }
.area-summary.warn { background: var(--orange-bg); color: var(--orange-fg); }
.area-message { padding: 10px 14px; border-radius: var(--r-sm); background: var(--accent-soft); margin-bottom: var(--sp-3); }
.area-error { padding: 10px 14px; border-radius: var(--r-sm); background: rgba(239, 68, 68, 0.14); color: #f87171; margin: var(--sp-2) 0; }
.area-error { padding: 10px 14px; border-radius: var(--r-sm); background: var(--red-bg); color: var(--red-fg); margin: var(--sp-2) 0; }
.area-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
.area-kv dt { color: var(--text-muted); }
.area-kv dd { margin: 0; overflow-wrap: anywhere; }
+13 -6
View File
@@ -2,25 +2,32 @@
chat-composer.css des Kundenportals; hier Farben (--chat-*), Verlauf, Sprechblasen und die Liste. */
.woms-chat {
--chat-accent: var(--accent);
--chat-accent-hover: var(--accent-2);
--chat-accent-hover: var(--accent-hover);
--chat-accent-soft: var(--accent-soft);
--chat-accent-line: rgba(16, 185, 129, 0.5);
--chat-accent-line: rgba(17, 17, 17, 0.14);
--chat-accent-text: var(--accent);
--chat-on-accent: #04130d;
--chat-surface: var(--surface-2);
--chat-on-accent: #ffffff;
--chat-surface: var(--bg);
--chat-line: var(--line-strong);
--chat-muted: var(--text-2);
display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0;
}
.woms-chat .chat-messages {
display: flex; flex-direction: column; gap: 8px; min-height: 280px; max-height: 60vh; overflow-y: auto;
padding: 12px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface);
padding: 12px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg);
}
.woms-chat .chat-bubble {
align-self: flex-start; max-width: 78%; padding: 8px 12px; border-radius: 14px;
background: var(--surface-2); color: var(--text); overflow-wrap: anywhere;
}
.woms-chat .chat-bubble.mine { align-self: flex-end; background: var(--accent-soft); border: 1px solid rgba(16, 185, 129, 0.35); }
.woms-chat .chat-bubble.mine { align-self: flex-end; background: var(--accent-soft); border: 1px solid var(--line); }
.woms-chat .chat-meta { display: block; margin-top: 4px; font-size: 11.5px; color: var(--text-muted); }
.woms-chat .chat-notice { font-size: 12px; }
/* Meldungen des Eingabebausteins (Stile aus chat-composer.css des Portals): Fehler rot, Info grau, beides lesbar auf Weiss */
.woms-chat .chat-composer-note,
.woms-chat .chat-tray-item.is-error .chat-file-size,
.woms-chat .chat-audio.is-error .chat-audio-time { color: var(--red-fg); }
.woms-chat .chat-composer-note.is-info { color: var(--text-2); }
/* Eingabefeld: Grundstil wie im frueheren Admin-Panel (chat-composer.css setzt nur das Layout) */
.woms-chat .chat-input-row textarea {
resize: none; min-height: 46px; max-height: 130px; padding: 11px 14px; border-radius: 14px;
+5 -5
View File
@@ -676,7 +676,7 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
.legal-tone-red td { background: rgba(239, 68, 68, 0.10); }
.legal-tone-red .legal-deadline { color: var(--danger); font-weight: 700; }
.legal-tone-orange td { background: rgba(245, 158, 11, 0.10); }
.legal-tone-orange .legal-deadline { color: #b45309; font-weight: 600; }
.legal-tone-orange .legal-deadline { color: var(--orange-fg); font-weight: 600; }
.legal-tone-missing .legal-deadline { color: var(--text-faint); font-style: italic; }
.legal-tone-grey td { color: var(--text-faint); }
.legal-detail td { background: var(--surface-2); cursor: default; }
@@ -909,13 +909,13 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
.fin-dialog:focus { outline: none; }
.fin-dialog-error {
margin-bottom: var(--sp-4); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-sm);
border: 1px solid var(--danger); background: rgba(239, 68, 68, 0.16); color: #fecaca; overflow-wrap: anywhere;
border: 1px solid var(--danger); background: var(--red-bg); color: var(--red-fg); overflow-wrap: anywhere;
}
.fin-dialog-warn { margin: 6px 0 0; font-size: 13px; color: #fbbf24; overflow-wrap: anywhere; }
.fin-dialog-warn { margin: 6px 0 0; font-size: 13px; color: var(--orange-fg); overflow-wrap: anywhere; }
.fin-dialog-sub { margin: 6px 0 0; font-size: 13px; color: var(--text-muted); overflow-wrap: anywhere; }
.fin-fixed-customer { margin: 0; overflow-wrap: anywhere; }
.fin-address-warn { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px var(--sp-3); font-size: 13px; overflow-wrap: anywhere; }
.fin-address-warn-dialog { margin: 6px 0 0; color: #fbbf24; }
.fin-address-warn-dialog { margin: 6px 0 0; color: var(--orange-fg); }
.fin-address-warn-page { margin: 0 0 var(--sp-4); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--warn); border-radius: var(--r-sm); color: var(--warn); }
.fin-address-warn .fin-link { font-size: 13px; }
.fin-items { border: 0; padding: 0; margin: 0 0 var(--sp-4); min-width: 0; }
@@ -932,7 +932,7 @@ a.list-row:hover .cell-title { color: var(--accent-ink); }
width: 38px; height: 40px; display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
background: transparent; border: 1px solid var(--border-strong); border-radius: 6px; color: var(--text-muted);
}
.fin-icon-btn:hover:not(:disabled) { color: #f87171; border-color: #f87171; }
.fin-icon-btn:hover:not(:disabled) { color: var(--red-fg); border-color: var(--red-fg); }
.fin-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.fin-icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.fin-items-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-3); }
+4 -4
View File
@@ -1,16 +1,16 @@
/* Serverpuls (Admin) */
.sp-page { display: flex; flex-direction: column; gap: var(--sp-4); }
.sp-error { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.4); color: #fca5a5; padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); }
.sp-error { background: var(--red-bg); border: 1px solid var(--danger-ring); color: var(--red-fg); padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); }
.sp-muted { color: var(--text-muted); }
.sp-small { font-size: 12px; }
.sp-head { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-4); }
.sp-level-medium { border-color: rgba(245,158,11,.5); }
.sp-level-critical { border-color: rgba(239,68,68,.6); }
.sp-level-medium { border-color: var(--orange-fg); }
.sp-level-critical { border-color: var(--danger); }
.sp-head-top { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.sp-headline { font-size: 18px; }
.sp-age { margin-left: auto; font-size: 13px; color: var(--text-muted); }
.sp-age-stale { color: #fbbf24; background: rgba(245,158,11,.14); padding: 2px 10px; border-radius: var(--r-pill); }
.sp-age-stale { color: var(--orange-fg); background: var(--orange-bg); padding: 2px 10px; border-radius: var(--r-pill); }
.sp-alerts { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.sp-alert { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline; padding: 8px 12px; border-radius: var(--r-sm); background: var(--surface-2); border-left: 3px solid var(--text-faint); }
.sp-alert-critical { border-left-color: var(--danger); }