fixes: timeAgo utc parse, reload clears unread, sync-event debug

- detail banner timeAgo: SQLite timestamps are UTC without tz marker,
  JS was parsing them as local. Fixed by appending Z when absent —
  same pattern chat uses. "2h ago" for a 30s-old update is gone.
- banner Reload button now re-fires detail.opened so the collab-buckets
  plugin clears the red dot on the board card and calls mark-card-seen.
  Dismiss (×) still leaves the dot — that's the acknowledgement that
  nothing was actually read.
- instrument /api/relay/poll with a sync_debug field so the JS can
  console.debug what happened on the sync-events sub-request (status,
  count, error body). Helps diagnose "no toast" reports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-14 23:16:48 +02:00
1 parent 1966f8f4c0
commit 4a668b7fc4
5 files changed
+26 -4

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
<?php <?php
define('APP_NAME', 'hyprcrd'); define('APP_NAME', 'hyprcrd');
define('APP_VERSION', '1.8.0'); define('APP_VERSION', '1.8.1');
// Database path — writable directory, outside webroot // Database path — writable directory, outside webroot
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation // DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hyprcrd", "name": "hyprcrd",
"version": "1.8.0", "version": "1.8.1",
"description": "hyprcrd — self-hosted card manager", "description": "hyprcrd — self-hosted card manager",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
+8 -1
View File
@@ -961,6 +961,7 @@ if (str_starts_with($route, '/api/')) {
// them. GET /api/sync-events on the relay auto-marks fetched events. // them. GET /api/sync-events on the relay auto-marks fetched events.
// Best-effort — older relays won't have the endpoint. // Best-effort — older relays won't have the endpoint.
$syncEvents = []; $syncEvents = [];
$syncDebug = ['requested' => true];
$sch = curl_init(rtrim($relayUrl, '/') . '/api/sync-events'); $sch = curl_init(rtrim($relayUrl, '/') . '/api/sync-events');
curl_setopt_array($sch, [ curl_setopt_array($sch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
@@ -970,14 +971,20 @@ if (str_starts_with($route, '/api/')) {
$sResp = curl_exec($sch); $sResp = curl_exec($sch);
$sStatus = curl_getinfo($sch, CURLINFO_HTTP_CODE); $sStatus = curl_getinfo($sch, CURLINFO_HTTP_CODE);
curl_close($sch); curl_close($sch);
$syncDebug['status'] = $sStatus;
if ($sStatus === 200) { if ($sStatus === 200) {
$decoded = json_decode($sResp, true); $decoded = json_decode($sResp, true);
if (is_array($decoded) && isset($decoded['events'])) { if (is_array($decoded) && isset($decoded['events'])) {
$syncEvents = $decoded['events']; $syncEvents = $decoded['events'];
$syncDebug['count'] = count($syncEvents);
} else {
$syncDebug['parse_error'] = substr((string)$sResp, 0, 200);
} }
} else {
$syncDebug['body'] = substr((string)$sResp, 0, 200);
} }
echo json_encode(['ok' => true, 'pulled' => $pulled, 'sync_events' => $syncEvents]); exit; echo json_encode(['ok' => true, 'pulled' => $pulled, 'sync_events' => $syncEvents, 'sync_debug' => $syncDebug]); exit;
} }
// List relay tenants (for the recipient picker) // List relay tenants (for the recipient picker)
+3
View File
@@ -340,6 +340,9 @@ function showSyncEventToast(ev) {
} }
function drainSyncEvents(relayRes) { function drainSyncEvents(relayRes) {
if (relayRes?.sync_debug) {
console.debug('[sync-events]', relayRes.sync_debug);
}
const events = Array.isArray(relayRes?.sync_events) ? relayRes.sync_events : []; const events = Array.isArray(relayRes?.sync_events) ? relayRes.sync_events : [];
for (const ev of events) showSyncEventToast(ev); for (const ev of events) showSyncEventToast(ev);
} }
+13 -1
View File
@@ -116,6 +116,10 @@ function showStaleBanner(fresh) {
state.viewingCard = current; state.viewingCard = current;
renderSidebar(current); renderSidebar(current);
showContent(current); showContent(current);
// Treat the reload as an explicit "I've now seen the new version"
// — re-fire detail.opened so collab-buckets clears the red dot
// on the board card and persists mark-card-seen to the backend.
window.hyprcrd?.hooks?.emit?.('detail.opened', { id: current.id });
} }
dismissStaleBanner(); dismissStaleBanner();
}); });
@@ -123,7 +127,15 @@ function showStaleBanner(fresh) {
} }
function timeAgo(iso) { function timeAgo(iso) {
const t = new Date(iso).getTime(); if (!iso) return '';
// SQLite stores CURRENT_TIMESTAMP as UTC without a tz marker
// ("2026-04-14 12:34:56"). Without an explicit Z, JS parses it as
// local time — off by the tz offset (2h on CEST, hence "2h ago"
// for a 30-second-old update).
const normalized = String(iso).includes('T') || /[Zz]|[+-]\d\d:?\d\d$/.test(iso)
? iso
: iso.replace(' ', 'T') + 'Z';
const t = new Date(normalized).getTime();
if (!isFinite(t)) return ''; if (!isFinite(t)) return '';
const s = Math.max(0, (Date.now() - t) / 1000); const s = Math.max(0, (Date.now() - t) / 1000);
if (s < 60) return `${Math.floor(s)}s ago`; if (s < 60) return `${Math.floor(s)}s ago`;