v0.6: cross-platform feed on the dashboard, home units, napkin

dashboard.html — Feed and Timeline swap places; Feed is every platform's
capture now, not Twitter intel, so rows carry their own platform glyph and an
unknown platform renders neutral rather than being dropped. A row whose ts is
really first_seen reads "seen X ago": saying "just now" for a post of unknown
age is a lie that sorts it to the top.

dashboard.json — regenerated by the refresher; _yt_caveat dropped, the
activate fix made it stale.

deploy/home — dash, openbridge, refresh service and timer for this desktop.
Refresh every 30min with --skip-foreground, not kdmirah's 10min full pass:
there is a human at this box and the metrics table only needs enough
resolution to watch engagement move. No Restart= on the oneshot — a down
browser is a waiting state the refresher records and exits 0 for.

NAPKIN.md — the whiteboard: measured delay budget, the background-render
table, the three landmines earned (a DOM signal is a rendering question, %z
cannot come back out of struct_time, a pure time sort is not a feed), and the
open calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-13 01:58:45 +08:00
co-authored by Claude Opus 5
parent 22e63ebf35
commit 15f3388e5e
7 changed files with 883 additions and 103 deletions
+49 -52
View File
@@ -330,25 +330,6 @@
.rail { display: flex; flex-direction: column; gap: 26px; }
.tweet {
padding: 11px 0;
border-bottom: 1px solid var(--line-soft);
}
.tweet:last-child { border-bottom: 0; }
.tweet .who {
font-size: 12.5px;
font-weight: 500;
color: var(--text);
overflow-wrap: anywhere;
}
.tweet .text {
font-size: 12.5px;
color: var(--text-dim);
margin-top: 3px;
overflow-wrap: anywhere;
}
.tweet .engagement { margin-top: 6px; }
/* ── empty / notice ────────────────────────────────────────────────── */
.empty {
@@ -397,17 +378,6 @@
background: var(--surface-2);
border-color: var(--line);
}
a.cardlink:hover .tweet,
a.cardlink:focus-visible .tweet {
background: var(--surface-2);
}
a.cardlink .tweet {
padding-left: 8px;
padding-right: 8px;
margin-left: -8px;
margin-right: -8px;
border-radius: var(--r-sm);
}
footer.foot {
margin-top: 34px;
@@ -460,21 +430,21 @@
<main>
<section>
<div class="shead">
<h2>Timeline</h2>
<span class="tool">create_post · get_notifications</span>
<span class="count" id="tl-count"></span>
<h2>Feed</h2>
<span class="tool">capture</span>
<span class="count" id="feed-count"></span>
</div>
<div class="timeline" id="timeline"></div>
<div id="feed"></div>
</section>
<aside class="rail">
<section>
<div class="shead">
<h2>Twitter intel</h2>
<span class="tool">get_feed</span>
<span class="count" id="feed-count"></span>
<h2>Timeline</h2>
<span class="tool">create_post · get_notifications</span>
<span class="count" id="tl-count"></span>
</div>
<div id="feed"></div>
<div class="timeline" id="timeline"></div>
</section>
</aside>
</main>
@@ -600,6 +570,14 @@
return new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric" });
};
// Some rows carry no platform clock — a Threads payload that arrived without
// taken_at, a notification whose only marker was "1d". The store fills `ts`
// with first_seen and flags it. Saying "just now" for a post of unknown age
// is a small lie that sorts it to the top, so say what the stamp actually is.
const stamp = row => row._tsIsFirstSeen
? "seen " + ago(parseTime(row))
: ago(parseTime(row));
// Engagement counts arrive as scraped strings ("1.2K") or numbers.
const countNum = v => {
if (typeof v === "number") return v;
@@ -669,6 +647,7 @@
who: BY_ID[pid] ? BY_ID[pid].name : String(d.platform),
text: d.text || "",
t: parseTime(d),
tsIsFirstSeen: !!d._tsIsFirstSeen,
url: d.url || d.link || null,
stats: {}
});
@@ -714,7 +693,7 @@
<div class="meta">
<span class="who">${esc(r.who)}</span>
<span class="kind ${r.kind === "activity" ? "" : r.kind}">${KIND_LABEL[r.kind]}</span>
<span class="ts">${esc(ago(r.t))}</span>
<span class="ts">${esc(r.tsIsFirstSeen ? "seen " + ago(r.t) : ago(r.t))}</span>
</div>
<div class="text${r.kind === "activity" ? " quiet" : ""}">${esc(r.text) || "<span style='color:var(--text-faint)'>(no text)</span>"}</div>
${r.detail ? `<div class="text quiet" style="font-size:12px">${esc(r.detail)}</div>` : ""}
@@ -724,7 +703,7 @@
}).join("");
}
// ── twitter intel ───────────────────────────────────────────────────
// ── feed — every platform capture writes here ───────────────────────
function renderFeed() {
const rows = state.feed.slice().sort((a, b) => (parseTime(b) ?? -Infinity) - (parseTime(a) ?? -Infinity));
@@ -732,22 +711,40 @@
if (!rows.length) {
$("feed").innerHTML = `<div class="empty">
No tweets pulled yet. <code>get_feed</code> writes scraped tweets
(author, text, time, likes, retweets, replies) to <code>feed</code>.
Nothing captured yet. <code>capture</code> reads the platform's own JSON
— the app's GraphQL responses on X and Threads, the prefetched payload
inside the Instagram document — and writes it to <code>feed</code>.
</div>`;
return;
}
$("feed").innerHTML = rows.map(t => linkWrap(t.url, `<div class="tweet">
<div class="who">${esc(t.author || "unknown")}</div>
<div class="text">${esc(t.text || "")}</div>
<div class="meta engagement">
<span><b>${esc(showCount(t.likes))}</b> likes</span>
<span><b>${esc(showCount(t.retweets ?? t.reposts))}</b> RT</span>
<span><b>${esc(showCount(t.replies))}</b> replies</span>
<span class="ts">${esc(ago(parseTime(t)))}</span>
</div>
</div>`, "twitter")).join("");
// Rows carry their own platform now, so a mixed feed still says which is
// which. A row from a platform the page does not know renders with a
// neutral glyph rather than being dropped.
$("feed").innerHTML = rows.map(t => {
const pid = norm(t.platform) || "twitter";
const p = BY_ID[pid];
const stripe = p ? p.color : "var(--line)";
const glyph = p
? `<span class="glyph ${p.id}">${esc(p.mark)}</span>`
: `<span class="glyph" style="background:var(--surface-2);color:var(--text-faint)">?</span>`;
return linkWrap(t.url, `<article class="card" style="--stripe:${stripe}">
${glyph}
<div class="body">
<div class="meta">
<span class="who">${esc(t.author || "unknown")}</span>
<span class="ts">${esc(stamp(t))}</span>
</div>
<div class="text">${esc(t.text || "") || "<span style='color:var(--text-faint)'>(no text)</span>"}</div>
${engagementHTML({
views: t.views,
likes: t.likes,
replies: t.replies,
reposts: t.reposts ?? t.retweets
})}
</div>
</article>`, pid);
}).join("");
}
// ── stats ───────────────────────────────────────────────────────────