Open incidents
diff --git a/public/plugins/oumta-dashboard/oumta-dashboard.js b/public/plugins/oumta-dashboard/oumta-dashboard.js
index 0a6ae8e..478ff69 100644
--- a/public/plugins/oumta-dashboard/oumta-dashboard.js
+++ b/public/plugins/oumta-dashboard/oumta-dashboard.js
@@ -269,19 +269,28 @@ function renderOverview() {
if (!o) return;
// Values that come directly from the Go overview endpoint [REAL].
- $('oumta-cluster-card').dataset.state = o.clusterState || 'unknown';
- $('oumta-cluster-state').textContent = o.clusterState || 'unknown';
+ const clusterCard = $('oumta-cluster-card');
+ const clusterState = o.clusterState || 'unknown';
+ clusterCard.dataset.state = clusterState;
+ clusterCard.classList.toggle('oumta-fail', clusterState === 'down');
+ $('oumta-cluster-state').textContent = clusterState;
$('oumta-cluster-updated').textContent = o.updatedAtUtc ? 'as of ' + formatTime(o.updatedAtUtc) : 'no data';
$('oumta-workloads-up').textContent = o.workloadsUp ?? '—';
$('oumta-workloads-down').textContent = o.workloadsDown ?? '—';
$('oumta-last-import').textContent = o.updatedAtUtc ? formatTime(o.updatedAtUtc) : '—';
+ // Any workload down → the "workloads down" tile becomes an alarm tile.
+ const downCard = $('oumta-workloads-down').closest('.oumta-card');
+ if (downCard) downCard.classList.toggle('oumta-fail', (o.workloadsDown ?? 0) > 0);
+
// Values derived client-side from the workloads list — the overview
// endpoint doesn't carry these today. If the backend later adds
// `incidents` / `attention` fields, prefer those over the derivation.
const attention = wl.filter(w => w.state !== 'healthy')
.sort((a, b) => rankState(a.state) - rankState(b.state));
$('oumta-incidents').textContent = attention.length || '—';
+ const incCard = $('oumta-incidents').closest('.oumta-card');
+ if (incCard) incCard.classList.toggle('oumta-fail', attention.length > 0);
const list = $('oumta-attention-list');
if (!attention.length) {
diff --git a/relay/index.php b/relay/index.php
index dd92dd8..ecf2f8f 100644
--- a/relay/index.php
+++ b/relay/index.php
@@ -66,7 +66,11 @@ if (str_starts_with($route, '/api/')) {
if ($method === 'OPTIONS') exit;
$contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
- $sizeLimit = ($route === '/api/attachments') ? 15728640 : 262144;
+ // Attachment routes handle binary blobs; everything else is JSON metadata.
+ // A ~150KB image becomes a ~275KB envelope after base64 + GCM framing,
+ // so 256KB would silently drop any non-trivial image on collab pushes.
+ $isAttachmentRoute = in_array($route, ['/api/attachments', '/api/attachments/upload', '/api/collab-attachments'], true);
+ $sizeLimit = $isAttachmentRoute ? 15728640 : 262144;
if ($contentLength > $sizeLimit) jsonResponse(['error' => 'payload too large'], 413);
// Each route file contains its handlers as if-blocks that exit on