v2.0.1 — collab-attachments size cap + oumta overview tiles
- relay: raise /api/collab-attachments + /api/attachments/upload POST size limit from 256KB to 15MB, matching /api/attachments. Previous limit silently dropped any collab image >~100KB as 413 (envelope overhead pushes even modest JPEGs past the cap), leaving puller clients with broken <img src="uploads/..."> references. - oumta-dashboard: centre overview tiles, bump value font to 56px, introduce .oumta-fail alarm state that paints the full tile red with white text. Toggled from JS for cluster state=down, workloads down >0, and open incidents >0. Fail colour uses --t-danger so it stays in sync with the "unavailable" workload labels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1dfffa5d6d
commit
72a7c7c7cc
6 files changed
+43
-9
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '2.0.0');
|
define('APP_VERSION', '2.0.1');
|
||||||
|
|
||||||
// APP_DEBUG: gates verbose error responses (curl traces, upstream bodies).
|
// APP_DEBUG: gates verbose error responses (curl traces, upstream bodies).
|
||||||
// Off by default; enable via env for local troubleshooting only.
|
// Off by default; enable via env for local troubleshooting only.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
border-radius: var(--ui-radius-card);
|
border-radius: var(--ui-radius-card);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
display: flex; flex-direction: column; gap: 4px;
|
display: flex; flex-direction: column; gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.2s, border-color 0.2s, color 0.2s;
|
||||||
}
|
}
|
||||||
.oumta-card-wide { grid-column: span 4; }
|
.oumta-card-wide { grid-column: span 4; }
|
||||||
.oumta-card-label {
|
.oumta-card-label {
|
||||||
@@ -57,18 +60,36 @@
|
|||||||
}
|
}
|
||||||
.oumta-card-value {
|
.oumta-card-value {
|
||||||
font-family: var(--font-content, monospace);
|
font-family: var(--font-content, monospace);
|
||||||
font-size: 28px;
|
font-size: 56px;
|
||||||
color: var(--t-bright);
|
color: var(--t-bright);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
.oumta-card-value-sm { font-size: 14px; font-weight: normal; }
|
.oumta-card-value-sm { font-size: 18px; font-weight: normal; line-height: 1.3; }
|
||||||
.oumta-card-sub { font-size: 10px; color: var(--t-dim); }
|
.oumta-card-sub { font-size: 10px; color: var(--t-dim); }
|
||||||
.oumta-danger { color: var(--t-danger); }
|
.oumta-danger { color: var(--t-danger); }
|
||||||
|
|
||||||
|
/* Fail state: whole card goes red; label, value, sub all flip to white
|
||||||
|
* so the box reads as a single alarm tile rather than a coloured number
|
||||||
|
* on dark chrome. Triggered from JS via the oumta-fail class. Uses the
|
||||||
|
* theme's --t-danger to stay consistent with the "unavailable" state
|
||||||
|
* labels and workload state colours elsewhere in the dashboard. */
|
||||||
|
.oumta-card.oumta-fail {
|
||||||
|
background: var(--t-danger);
|
||||||
|
border-color: var(--t-danger);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.oumta-card.oumta-fail .oumta-card-label,
|
||||||
|
.oumta-card.oumta-fail .oumta-card-value,
|
||||||
|
.oumta-card.oumta-fail .oumta-card-sub {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.oumta-cluster-status[data-state="healthy"] .oumta-card-value { color: #4ade80; }
|
.oumta-cluster-status[data-state="healthy"] .oumta-card-value { color: #4ade80; }
|
||||||
.oumta-cluster-status[data-state="degraded"] .oumta-card-value { color: #fb923c; }
|
.oumta-cluster-status[data-state="degraded"] .oumta-card-value { color: #fb923c; }
|
||||||
.oumta-cluster-status[data-state="down"] .oumta-card-value { color: var(--t-danger); }
|
|
||||||
.oumta-cluster-status[data-state="unknown"] .oumta-card-value { color: var(--t-dim); }
|
.oumta-cluster-status[data-state="unknown"] .oumta-card-value { color: var(--t-dim); }
|
||||||
|
/* data-state="down" is handled by .oumta-fail (toggled from JS) so the
|
||||||
|
* cluster tile goes fully red + white like the other alarms. */
|
||||||
|
|
||||||
/* ── Attention list ── */
|
/* ── Attention list ── */
|
||||||
.oumta-section { margin-top: 8px; }
|
.oumta-section { margin-top: 8px; }
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="oumta-card">
|
<div class="oumta-card">
|
||||||
<div class="oumta-card-label">Workloads down</div>
|
<div class="oumta-card-label">Workloads down</div>
|
||||||
<div class="oumta-card-value oumta-danger" id="oumta-workloads-down">—</div>
|
<div class="oumta-card-value" id="oumta-workloads-down">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="oumta-card">
|
<div class="oumta-card">
|
||||||
<div class="oumta-card-label">Open incidents</div>
|
<div class="oumta-card-label">Open incidents</div>
|
||||||
|
|||||||
@@ -269,19 +269,28 @@ function renderOverview() {
|
|||||||
if (!o) return;
|
if (!o) return;
|
||||||
|
|
||||||
// Values that come directly from the Go overview endpoint [REAL].
|
// Values that come directly from the Go overview endpoint [REAL].
|
||||||
$('oumta-cluster-card').dataset.state = o.clusterState || 'unknown';
|
const clusterCard = $('oumta-cluster-card');
|
||||||
$('oumta-cluster-state').textContent = o.clusterState || 'unknown';
|
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-cluster-updated').textContent = o.updatedAtUtc ? 'as of ' + formatTime(o.updatedAtUtc) : 'no data';
|
||||||
$('oumta-workloads-up').textContent = o.workloadsUp ?? '—';
|
$('oumta-workloads-up').textContent = o.workloadsUp ?? '—';
|
||||||
$('oumta-workloads-down').textContent = o.workloadsDown ?? '—';
|
$('oumta-workloads-down').textContent = o.workloadsDown ?? '—';
|
||||||
$('oumta-last-import').textContent = o.updatedAtUtc ? formatTime(o.updatedAtUtc) : '—';
|
$('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
|
// Values derived client-side from the workloads list — the overview
|
||||||
// endpoint doesn't carry these today. If the backend later adds
|
// endpoint doesn't carry these today. If the backend later adds
|
||||||
// `incidents` / `attention` fields, prefer those over the derivation.
|
// `incidents` / `attention` fields, prefer those over the derivation.
|
||||||
const attention = wl.filter(w => w.state !== 'healthy')
|
const attention = wl.filter(w => w.state !== 'healthy')
|
||||||
.sort((a, b) => rankState(a.state) - rankState(b.state));
|
.sort((a, b) => rankState(a.state) - rankState(b.state));
|
||||||
$('oumta-incidents').textContent = attention.length || '—';
|
$('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');
|
const list = $('oumta-attention-list');
|
||||||
if (!attention.length) {
|
if (!attention.length) {
|
||||||
|
|||||||
+5
-1
@@ -66,7 +66,11 @@ if (str_starts_with($route, '/api/')) {
|
|||||||
if ($method === 'OPTIONS') exit;
|
if ($method === 'OPTIONS') exit;
|
||||||
|
|
||||||
$contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
|
$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);
|
if ($contentLength > $sizeLimit) jsonResponse(['error' => 'payload too large'], 413);
|
||||||
|
|
||||||
// Each route file contains its handlers as if-blocks that exit on
|
// Each route file contains its handlers as if-blocks that exit on
|
||||||
|
|||||||
Reference in new issue
Block a user