Changeset a7bcf66 in Klonkt


Ignore:
Timestamp:
07/28/2026 06:30:43 PM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
70677e96
Parents:
d9ad6c5
Message:

De Guardian PWA liep twee uur achter

Op sound-fabrics staat de tijdzone op Europe/Amsterdam, maar een hulpvraag van
20:20 stond in de PWA als 18:20. Het dashboard bouwt zijn kaarten in de browser
en sneed de rauwe UTC-string af (slice(0,16)) in plaats van hem om te rekenen.
De server geeft de tijd nu geformatteerd mee, met dezelfde formatDateTime die de
Krant en Berichten gebruiken; het afsnijden blijft alleen als terugval staan.

In formatDateTime zat een tweede probleem, dat nog niet zichtbaar was. SQLite
schrijft CURRENT_TIMESTAMP als UTC zonder dat erbij te zeggen ("2026-07-28
18:20:33"), en new Date() leest een string in die vorm als LOKALE tijd. Dat gaat
goed zolang de machine op UTC staat, wat nu toevallig zo is. Zet de VPS ooit op
Amsterdam en elke opgeslagen datum in de hele app schuift twee uur op. De parser
zegt nu expliciet UTC.

Onderweg bleek Berichten en de PWA ook niet dezelfde tijd te tonen voor dezelfde
post: 20:12 tegenover 20:20. Berichten liet zien wanneer wij de post ontvingen,
de PWA en de Krant wanneer hij geschreven is. Berichten toont nu ook de
publicatietijd. Sorteren en de "nieuw sinds je laatste bezoek"-stip blijven op
de ontvangsttijd: een post die laat federeert is nog steeds nieuw voor jou.

Changed files:
src/middleware/render.js

  • parseStamp leest een tijdstempel zonder zone als UTC
  • formatDateTime geexporteerd voor oppervlakken buiten de EJS-pagina's

src/routes/guardian.js

  • when_text bij hulpvragen en bij de tijdlijn van de wards

src/assets/js/guardian.js

  • when() gebruikt dat veld, afsnijden alleen nog als terugval

src/services/ActivityPubService.js

  • getNotifications geeft published mee naast created_at

src/views/partials/msg-item.ejs

  • toont de publicatietijd, met created_at als terugval

New file:
test/timestamps.test.js

  • de tijdzone-instelling wordt toegepast, en een SQLite-tijdstempel hangt niet af van de tijdzone van de machine

remarks: geverifieerd op de wegwerp-database met Europe/Amsterdam: Berichten en
de PWA tonen allebei 28 jul 2026, 20:20 voor dezelfde post.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/guardian.js

    rd9ad6c5 ra7bcf66  
    3232    catch (e) { return uri; }
    3333  }
    34   function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); }
     34  // The server hands over a timestamp already formatted in the site's timezone
     35  // (Beheer -> Instellingen), the same clock de Krant and Berichten show. The
     36  // slice is only a fallback for a row that predates that field: it shows raw
     37  // UTC, which is what made a 20:20 call for help read 18:20.
     38  function when(item, raw) {
     39    if (item && item.when_text) return item.when_text;
     40    return String(raw || '').slice(0, 16).replace('T', ' ');
     41  }
    3542  function show(id, on) { document.getElementById(id).hidden = !on; }
    3643
     
    4956      else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
    5057      row.appendChild(who);
    51       row.appendChild(el('span', 'when', when(h.published || h.created_at)));
     58      row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
    5259      card.appendChild(row);
    5360      var body = el('div', 'body g-note');
     
    197204      var head = el('div', 'row');
    198205      head.appendChild(el('span', 'who grow', p.author));
    199       if (p.published) head.appendChild(el('span', 'g-when', when(p.published)));
     206      if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
    200207      card.appendChild(head);
    201208      var body = el('div', 'feed-body');
  • src/middleware/render.js

    rd9ad6c5 ra7bcf66  
    8585const siteTimezone = () => getSetting('timezone') || undefined;
    8686
     87/**
     88 * Read a stored timestamp as the moment it actually is.
     89 *
     90 * SQLite's CURRENT_TIMESTAMP writes UTC without saying so ("2026-07-28
     91 * 18:20:33"), and new Date() reads a string in that shape as LOCAL time. That
     92 * is right only as long as the server runs on UTC; set the machine to
     93 * Europe/Amsterdam and every stored date silently shifts two hours. So say UTC
     94 * out loud. Anything already carrying a zone (AP `published` ends in Z) is left
     95 * to the normal parser.
     96 */
     97const parseStamp = (v) => {
     98  if (!v) return null;
     99  const s = String(v);
     100  const d = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/.test(s)
     101    ? new Date(`${s.replace(' ', 'T')}Z`)
     102    : new Date(s);
     103  return Number.isNaN(d.getTime()) ? null : d;
     104};
     105
    87106const formatDate = (iso) => {
    88   if (!iso) return '';
    89   return new Date(iso).toLocaleDateString('nl-NL', { timeZone: siteTimezone(), day: 'numeric', month: 'long', year: 'numeric' });
     107  const d = parseStamp(iso);
     108  return d ? d.toLocaleDateString('nl-NL', { timeZone: siteTimezone(), day: 'numeric', month: 'long', year: 'numeric' }) : '';
    90109};
    91110
    92 const formatDateTime = (iso) => {
    93   if (!iso) return '';
    94   return new Date(iso).toLocaleString('nl-NL', { timeZone: siteTimezone(), dateStyle: 'medium', timeStyle: 'short' });
     111/** A timestamp in the site's own timezone (Beheer → Instellingen). Exported so
     112 *  surfaces outside the EJS pages (the Guardian PWA) read the same clock. */
     113export const formatDateTime = (iso) => {
     114  const d = parseStamp(iso);
     115  return d ? d.toLocaleString('nl-NL', { timeZone: siteTimezone(), dateStyle: 'medium', timeStyle: 'short' }) : '';
    95116};
    96117
  • src/routes/guardian.js

    rd9ad6c5 ra7bcf66  
    1919import * as Guardianship from '../services/guardianship/index.js';
    2020import { t as i18nT, resolveLang } from '../services/i18n.js';
    21 import { injectCspNonce, renderNoteBody } from '../middleware/render.js';
     21import { injectCspNonce, renderNoteBody, formatDateTime } from '../middleware/render.js';
    2222import { emojiName } from '../services/NoteRender.js';
    2323
     
    6262    body_html: renderNoteBody(h, L),
    6363    name_html: emojiName(h.actor_name || '', h.actor_emoji_json),
     64    // In the site's own timezone, the same as everywhere else in Klonkt. The
     65    // PWA used to slice the raw UTC string, so a 20:20 call for help read 18:20.
     66    when_text: formatDateTime(h.published || h.created_at),
    6467  }));
    6568  return {
     
    141144      url: p.url,
    142145      published: p.published || p.created_at,
     146      when_text: formatDateTime(p.published || p.created_at),
    143147      cw: p.cw || null,
    144148      media: p.media_json ? JSON.parse(p.media_json) : [],
  • src/services/ActivityPubService.js

    rd9ad6c5 ra7bcf66  
    35213521  try {
    35223522    const rows = db.prepare(`
    3523       SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.visibility,
     3523      SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.published, i.visibility,
    35243524             i.emoji_json, i.actor_emoji_json, i.media_json, i.quote_json, i.embed_json,
    35253525             p.slug AS post_slug, p.title AS post_title
     
    35313531      type: r.kind, name: r.actor_name, handle: r.actor_handle, url: r.actor_url, icon: r.actor_icon,
    35323532      content: stripLeadingMentions(r.content), post_slug: r.post_slug, post_title: r.post_title, created_at: r.created_at,
     3533      // When the post was written, for display. created_at (when it reached us)
     3534      // stays the sort key and the unread watermark: a note that federated late
     3535      // is still new to you.
     3536      published: r.published,
    35333537      emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json,   // FEP-9098 (messages render)
    35343538      media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json,   // rendered like a Krant post
     
    35553559  } catch { /* ignore */ }
    35563560  try {
    3557     for (const r of db.prepare(`SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, help_request, created_at,
     3561    for (const r of db.prepare(`SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, help_request, created_at, published,
    35583562                                       emoji_json, actor_emoji_json, media_json, quote_json, embed_json
    35593563                                FROM ap_mentions WHERE slug = ? ORDER BY created_at DESC LIMIT ?`).all(slug, L)) {
    3560       out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, help_request: r.help_request ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at,
     3564      out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, help_request: r.help_request ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at, published: r.published,
    35613565        // Same trimmings a Krant row has, so Berichten renders the post identically.
    35623566        emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json, media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json });
  • src/views/partials/msg-item.ejs

    rd9ad6c5 ra7bcf66  
    4444              <% } %>
    4545              <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
    46               <% if (n.created_at) { %><span class="msg-time"><%= formatDateTime(n.created_at) %></span><% } %>
     46              <% /* When the post was written, like de Krant and de Guardian PWA
     47                    show it. created_at (when it reached us) stays the sort key
     48                    and drives the "new since your last visit" dot above. */ %>
     49              <% if (n.published || n.created_at) { %><span class="msg-time"><%= formatDateTime(n.published || n.created_at) %></span><% } %>
    4750            </div>
    4851
Note: See TracChangeset for help on using the changeset viewer.