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:
+49
-52
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user