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:
@@ -0,0 +1,429 @@
|
||||
# NAPKIN — socialmag, going programmatic
|
||||
|
||||
Whiteboard for the couch talk, 2026-09-12 ~03:50. Numbers here were measured
|
||||
tonight on this box against abang's live Brave (`:9336`), not estimated.
|
||||
Nothing in step 1–3 below is built yet.
|
||||
|
||||
---
|
||||
|
||||
## Where we are tonight
|
||||
|
||||
Running at home right now, all by hand:
|
||||
|
||||
| Piece | State |
|
||||
|---|---|
|
||||
| dashboard `:8321` | up — `http://127.0.0.1:8321/socialmag/dashboard.html` |
|
||||
| openbridge `:8322` | up (started 03:24; it was **down**, which is why notification clicks opened new tabs instead of reusing the platform tab) |
|
||||
| brave-abang CDP `:9336` | up — not a service, abang launches it |
|
||||
| `refresher.py` | works, one-shot, **pressed by hand** |
|
||||
|
||||
Shipped tonight, unplanned:
|
||||
|
||||
- `facebook.check_login` and `youtube.check_login` were **lying OUT on a signed-in
|
||||
account**. Both were DOM-signal checks. Swapped to JS state (`c_user` cookie,
|
||||
`ytcfg.get('LOGGED_IN')`). 4-in/2-out → **6-in/0-out**.
|
||||
- `refresher.fetch_content()` added — `notifications` and `feed` now come from a
|
||||
real scrape instead of being hand-pasted. Boris Cherny's 18h-old tweet gone;
|
||||
feed reads 22m/54m/1h.
|
||||
- Backups in `Archived/` (`facebook.py`, `youtube.py`, `refresher.py`,
|
||||
`dashboard.json`, all `20260912-03*`).
|
||||
|
||||
---
|
||||
|
||||
## The delay budget — where 97 seconds goes
|
||||
|
||||
One full refresher pass, 8 jobs, measured 03:35:01 → 03:36:38.
|
||||
|
||||
| Source | Cost | Kind |
|
||||
|---|---|---|
|
||||
| `PostQueue._space_out()` | 3.0–6.0s **between every job** → 7 gaps ≈ 21–42s | deliberate anti-bot pacing — **keep** |
|
||||
| `check_login` × 6 | navigate + `asyncio.sleep(4)` (fb/threads/twitter) or `sleep(5)` (ig/tiktok/yt) | blind sleep |
|
||||
| `get_feed` | `sleep(4)` + click Following + `sleep(3)` + scroll + `sleep(2)` + `sleep(1)` = **10s fixed** | blind sleep |
|
||||
| `get_notifications` | navigate + `sleep(4)` | blind sleep |
|
||||
| CDP attach + activate + real navigate | ~3–8s per platform | real work |
|
||||
|
||||
**~55 of 97 seconds is unconditional `sleep()`.** Not a slow network — the code
|
||||
asks for it.
|
||||
|
||||
X measured alone, warm tab, 3 runs: `get_feed` **15.0 / 11.2 / 11.1s**,
|
||||
`check_login` **4.8 / 5.2 / 5.1s**. Of the 11.1s, **10.0s is fixed sleep** — the
|
||||
actual DOM scrape of 10 articles takes ~1.1s.
|
||||
|
||||
The post flows already poll properly (`threads._wait_for_composer`,
|
||||
`tiktok._wait_for_js`, `youtube._wait_for_editor_empty`). Only the **read** flows
|
||||
sleep blind. History, not a technical reason.
|
||||
|
||||
---
|
||||
|
||||
## Background rendering — measured, not assumed
|
||||
|
||||
Question: can it work without stealing Brave's focus? Parked focus elsewhere,
|
||||
attached without `activate()`:
|
||||
|
||||
| Platform | `visibilityState` | DOM signal | JS-state signal |
|
||||
|---|---|---|---|
|
||||
| twitter | `hidden` | ✅ True | ✅ True — **full scrape works**, 10 tweets, 11 articles at t=2s, flat to t=12s |
|
||||
| ig | `hidden` | ✅ True | ✅ cookie `ds_user_id` |
|
||||
| threads | `hidden` | 🔴 **False** | ✅ cookie `ds_user_id` |
|
||||
| fb | — | 🔴 False | ✅ cookie `c_user` |
|
||||
| yt | — | 🔴 False | ✅ `ytcfg.LOGGED_IN` |
|
||||
| tiktok | **unproven** | True | ❌ `sessionid` is httpOnly, invisible to `document.cookie` |
|
||||
|
||||
🔴 **Threads is the third liar**, same shape as fb and yt: the "What's new?"
|
||||
composer never mounts in a hidden tab, so a signed-in account reads as signed out.
|
||||
|
||||
⚠️ **TikTok could not be backgrounded.** Activated a sibling tab in the *same*
|
||||
window (`Browser.getWindowForTarget` → both `windowId` 915141824) and TikTok still
|
||||
reported `visible`. Either it lives in its own window, or it overrides
|
||||
`visibilityState` to keep video playing. Its DOM signal has never lied — but it has
|
||||
never been tested in a genuine background either. **Three states, not two: keep
|
||||
`activate()` on tiktok until there is evidence.**
|
||||
|
||||
The headline: X renders in **2 seconds** in a hidden tab while the code sleeps
|
||||
**10**. Dropping focus is not the only prize — the sleeps go with it.
|
||||
|
||||
---
|
||||
|
||||
## Plan — three steps, each stands alone
|
||||
|
||||
### 1 · Background pass *(smallest, highest value; nothing else is tolerable without it)*
|
||||
|
||||
- `threads.check_login` → cookie. **Required** — it lies today.
|
||||
- `get_or_open_tab(url, contains, activate=True)` — add the parameter, don't
|
||||
remove the behaviour. **Post flows still need foreground** for file dialogs.
|
||||
- Read paths pass `activate=False`: five `check_login`s, `get_notifications`,
|
||||
`get_feed`. TikTok stays `True`.
|
||||
- Fixed sleeps → `wait_for` in the three read flows.
|
||||
|
||||
Estimate: **97s → ~25-30s, zero tab jumps.**
|
||||
|
||||
### 2 · SQLite behind the refresher
|
||||
|
||||
Not for size — 4.2KB today, JSON holds that forever. JSON breaks because **every
|
||||
refresh replaces `feed` and `notifications` wholesale**:
|
||||
|
||||
- No history. *"How often does a session drop?"* — the exact question README says
|
||||
we need answered before judging the VPS version — is unanswerable.
|
||||
- No concept of new. Tonight's three Threads rows are yesterday's three rows with
|
||||
`11h` aged to `1d`. Nothing knows they are the same.
|
||||
- 🔴 The derived `ts` decays. A row sinks an hour every hour. **`first_seen` is a
|
||||
timestamp we own** — exact, fixed, never guessed. Threads' missing clock stops
|
||||
being a problem instead of being patched.
|
||||
|
||||
Shape: stdlib `sqlite3`, one file, `id = sha1(platform|text|url)`,
|
||||
`first_seen` / `last_seen` / `times_seen`. Refresher writes the DB, then emits
|
||||
`dashboard.json` as a derived snapshot — **`dashboard.html` does not change**.
|
||||
|
||||
Not proposed: artifact `db` mode. The page supports it (`onSnapshot` over four
|
||||
collections) but it moves the data out of the house and fixes none of the three.
|
||||
|
||||
### 3 · The timer
|
||||
|
||||
`deploy/kdmirah/socialmag-refresh.timer` already exists —
|
||||
`OnUnitActiveSec=10min`, `Persistent=true`, `OnBootSec=2min`. Written for
|
||||
`User=ijat` on kdmirah, **never installed here**. Adapt paths and user.
|
||||
|
||||
Blocked on step 1 only. Installing it today means tabs jumping every 10 minutes
|
||||
all day, on the desktop abang is working at.
|
||||
|
||||
---
|
||||
|
||||
# ✅ ALL THREE STEPS BUILT — 2026-09-12 04:10-06:00
|
||||
|
||||
The plan above is history now. What actually shipped, and where it differs:
|
||||
|
||||
| Step | State |
|
||||
|---|---|
|
||||
| 1 · background pass | ✅ `activate=False` param on `get_or_open_tab`; threads/ig/twitter moved to JS-state signals; tiktok deliberately left foreground |
|
||||
| 2 · SQLite | ✅ `store.py` — `platform_checks` / `items` / `metrics`. `dashboard.json` is a derived view; `dashboard.html` unchanged for it |
|
||||
| 3 · timer | ✅ three units in `deploy/home/`, installed and verified. Refresh every **30min** (not kdmirah's 10), `--skip-foreground` |
|
||||
| — | ➕ `capture.py` — the third lane, which was not in the plan at all |
|
||||
|
||||
**The `wait_for` rewrite of `get_feed` was never done, on purpose.** `capture`
|
||||
obsoleted the DOM read path before that work would have paid for itself.
|
||||
|
||||
## What changed, by file
|
||||
|
||||
cdp.py:179 get_or_open_tab(..., activate=True) — added, not replaced
|
||||
platforms/threads.py DOM composer → ds_user_id cookie (it was LYING)
|
||||
platforms/instagram.py → ds_user_id
|
||||
platforms/twitter.py → twid
|
||||
platforms/facebook.py → c_user, fixed sleep dropped
|
||||
platforms/youtube.py → ytcfg.LOGGED_IN, fixed sleep dropped
|
||||
platforms/tiktok.py untouched; a comment says why it stays foreground
|
||||
refresher.py parallel check_all(skip_foreground=), capture wired in,
|
||||
store wired in
|
||||
capture.py NEW — network capture + in-document read
|
||||
store.py NEW — SQLite
|
||||
dashboard.html columns swapped, feed is cross-platform, "seen X ago"
|
||||
deploy/home/* three units
|
||||
|
||||
Measured: **97s → 34s** full pass, **9-10s** login-only, 3/3 stable.
|
||||
|
||||
## Landmines earned tonight
|
||||
|
||||
- 🔴 **Dropping a `sleep` is not free.** Removing X's `sleep(4)` made `check_login`
|
||||
report OUT on a signed-in account once five tabs navigated at once — the DOM
|
||||
signal had not painted. Fixed with the `twid` cookie, not by restoring the sleep.
|
||||
- 🔴 **`%z` goes into `struct_time` and cannot come back out.** `_x_time_to_iso`
|
||||
emitted `2026-09-11T19:41:10` with no offset; JS `Date.parse` reads a naked
|
||||
datetime as *local*, shifting every X row by the machine's offset, silently.
|
||||
Use `datetime.strptime(...).isoformat()`.
|
||||
- 🔴 **A pure time sort is not a cross-platform feed.** X returns ~185 fresh rows a
|
||||
pass against Instagram's 3 older ones, so IG existed in the database and never
|
||||
on the page. `FEED_PER_PLATFORM = 10` — representation before the merge.
|
||||
- 🔴 **`ts` must be written once and never rewritten** (`COALESCE(ts, ?)`).
|
||||
Re-deriving a Threads notification's time each pass reintroduces exactly the
|
||||
decay the table exists to stop.
|
||||
- 🔴 **"just now" for a post of unknown age is a lie that sorts to the top.** A
|
||||
Threads payload without `taken_at` falls back to `first_seen`; those rows read
|
||||
**"seen X ago"** now.
|
||||
- ⚠️ `bravemag` `capture.screenshot` fails with an **empty** error string — the old
|
||||
stdout/stderr trap. DOM probes (`page.evaluate` for bounding boxes and counts)
|
||||
proved the layout instead, and are better evidence anyway.
|
||||
|
||||
---
|
||||
|
||||
## Open, abang's call
|
||||
|
||||
- **`posts` has no writer.** Only a real `create_post` can produce it, so the
|
||||
"posted" rows in the timeline stay hand-written until something records post
|
||||
results. Does `create_post` write to the DB in step 2?
|
||||
- **Five platforms have no `get_notifications` at all** — `server.py:397` is
|
||||
`{"threads": threads}`. fb / ig / yt / tiktok / twitter raise. Worth building,
|
||||
or is Threads + X feed enough for the story?
|
||||
- **TikTok's background behaviour** — worth one proper probe (own window? spoofed
|
||||
`visibilityState`?), or just leave `activate()` on it forever?
|
||||
- **Cadence.** 10min is kdmirah's number, chosen for a box nobody watches. On this
|
||||
desktop, with step 1 done, what is it worth?
|
||||
- **Units for `:8321` and `:8322`** — both hand-started today, both die on reboot.
|
||||
`deploy/kdmirah/` has both service files ready to adapt.
|
||||
|
||||
---
|
||||
|
||||
## Landmines carried forward
|
||||
|
||||
- 🔴 **A DOM signal on a social platform is a rendering question, not a login
|
||||
question.** Three platforms proved it tonight. Reach for JS state first.
|
||||
- 🔴 **A short `sleep()` returns a short list with no error** — indistinguishable
|
||||
from "nothing happened". This is why `fetch_content` keeps old rows and records
|
||||
`_refreshed.contentErrors` instead of writing `[]`.
|
||||
- ⚠️ `SOCIALMAG_CDP_PORT` is read by **nothing**. `/root/.claude.json` sets it.
|
||||
Only `SOCIALMAG_CDP_ENDPOINT` is consulted (`server.py:65`).
|
||||
- ⚠️ Run as `python3 -m socialmag`, never `socialmag/server.py` — `selectors.py`
|
||||
and `queue.py` shadow stdlib modules asyncio imports.
|
||||
- ⚠️ openbridge being down is **silent** by design (progressive enhancement). The
|
||||
feature looks deleted rather than broken. That is how tonight started.
|
||||
- ⚠️ **Escaping a JS regex through a Python heredoc bit me tonight.**
|
||||
`"/ds_user_id=\\d+/"` reaches JS as a literal backslash and never matches, so
|
||||
three platforms polled the full 20s and reported signed-out. The first
|
||||
parallel benchmark measured three fake timeouts. A test that fails the way
|
||||
the real thing would fail is the dangerous kind.
|
||||
|
||||
---
|
||||
|
||||
# The destination — Blotato, without the API
|
||||
|
||||
`https://www.blotato.com` — unified social API + MCP, $29/mo, 9 platforms.
|
||||
Abang's target shape. **Their moat is the OAuth app approvals**, which is their
|
||||
heaviest operational cost and the thing "without an API" skips entirely.
|
||||
|
||||
| # | Blotato block | socialmag today | Cost without an API |
|
||||
|---|---|---|---|
|
||||
| 01 | PUBLISH — 9 platforms, one call | **6 platforms, shipped** | LinkedIn, Bluesky, Pinterest — one module each, pattern exists |
|
||||
| 02 | SCHEDULE — queue, next open slot, weekly slots | none | **Cheap.** Local queue + timer. Never touches the browser problem |
|
||||
| 03 | COMMENTS & DMs — read inbox, reply, DM | Threads notifications, `twitter.reply` | **Expensive via DOM** — every inbox is a different page. Cheap via private API (see below) |
|
||||
| 04 | DM AUTOMATIONS — keyword triggers | none | Needs 03. Highest account risk of anything here |
|
||||
| 05 | ANALYTICS — top posts, snapshot history | `SCRAPE_JS` already pulls likes/RT/replies | **Cheap — this is step 2.** "Snapshot history" *is* `first_seen`/`last_seen` |
|
||||
| 06 | VISUALS — infographics, carousels, video | none | Separate lane. House already has geminigen, piskelmag, blendmag |
|
||||
| 07 | SOURCES — YouTube/PDF/article → post | none | Separate lane. An LLM + fetch problem, not a browser one |
|
||||
| 08 | API & MCP | **socialmag is already an MCP server** | Done |
|
||||
|
||||
Steps 1–3 aren't changed by this — they got a destination. Block 02 *is* the
|
||||
timer with a queue on it. Block 05 *is* the SQLite.
|
||||
|
||||
🔴 **Blotato has platform permission; we do not.** They call official APIs. We
|
||||
drive a browser logged in as abang, from one home IP. So throughput is not their
|
||||
1,800 req/hr — it is a sequential queue with 3–6s gaps, one tab at a time, and
|
||||
that gap *is* the disguise. The risk of a login challenge lands on abang's six
|
||||
real accounts, not on a service account. This is why the design is **single-seat,
|
||||
not SaaS.** Do not chase their numbers.
|
||||
|
||||
---
|
||||
|
||||
# Parallel — CDP supports it, and our client already does
|
||||
|
||||
Measured tonight, 6 platforms, all six returning True:
|
||||
|
||||
| | Wall |
|
||||
|---|---|
|
||||
| Sequential (with polling) | **19.0s** |
|
||||
| Parallel (`asyncio.gather`) | **6.1s** |
|
||||
| | **3.1×** |
|
||||
|
||||
Against the 97s the code takes today, parallel + poll + background is **~16×**.
|
||||
|
||||
CDP is built for this: one WebSocket, `flatten: True`, a `sessionId` per tab, an
|
||||
`id` on every command. `cdp.py:127-143` already keeps futures in a dict keyed by
|
||||
id with a single `_reader` dispatching replies. **Nothing needs rewriting.**
|
||||
|
||||
🔴 **The bigger finding is in the poll column: every signal read `0.0s`.** It was
|
||||
present the moment `navigate()` returned. `asyncio.sleep(4)` has been waiting for
|
||||
something that had already happened.
|
||||
|
||||
**Where parallel is allowed — abang's call, not a technical one:**
|
||||
|
||||
- `check_login` — **parallel, agreed.** Read-only, no interaction, no writes. Six
|
||||
tabs loading at once looks like a session restore. 19s → 6s.
|
||||
- Post flows — **stay sequential with the gap.** That is where the pacing actually
|
||||
works, and where one mistake is outward-facing.
|
||||
- `get_feed` / `get_notifications` — parallel is fine, but it is two jobs; the win
|
||||
is small.
|
||||
|
||||
---
|
||||
|
||||
# The other lane — private APIs (abang's two links)
|
||||
|
||||
⚠️ **`aegents/twitter-no-api` is a fork of `d60/twikit`**, created Dec 2024 and
|
||||
untouched since; its README is twikit's, badges and all. Take the upstream:
|
||||
**`d60/twikit` — 4,667★, pushed 2026-03-10, MIT, active.**
|
||||
|
||||
**`subzeroid/aiograpi-rest` — 640★, pushed 2026-05-21, MIT.** REST boundary over
|
||||
`aiograpi` (the maintained async fork of `instagrapi`) = Instagram's **private
|
||||
mobile API**. Full write surface: post, DM, story. First-class
|
||||
`challenge_required` and 2FA handling.
|
||||
|
||||
These are a **different architecture**, not a socialmag upgrade:
|
||||
|
||||
| | socialmag | twikit / aiograpi |
|
||||
|---|---|---|
|
||||
| How | drives abang's already-logged-in Brave over CDP | logs in with username+password, speaks the app's own internal API |
|
||||
| Stores a password? | **No** — the session belongs to Brave | **Yes**, required |
|
||||
| Needs a browser / display? | Yes | **No** — headless, server, VPS |
|
||||
| Data | scraped DOM | structured JSON, pydantic-typed |
|
||||
| Breaks when | the platform redesigns its DOM | the platform moves an internal endpoint |
|
||||
| Ban vector | traffic pattern | **unofficial client + login from a new IP** |
|
||||
|
||||
**Why this matters to the plan:** these libraries solve exactly what README's
|
||||
"Running on a VPS" section says blocks us — no browser, no display, no tab, no
|
||||
focus. Block 03 (Comments & DMs), which is expensive through the DOM, gets cheap
|
||||
through `aiograpi`, because IG DMs have a real endpoint.
|
||||
|
||||
🔴 **The price, stated once: both require storing the real account passwords and
|
||||
logging in as an unofficial client.** That is the classic path to a challenge or
|
||||
a lock. socialmag never holds a password — that is *why* it is slow.
|
||||
|
||||
**The hybrid worth discussing on the couch:** private API for read-heavy and
|
||||
scheduled work (analytics polling, inbox reads) where volume is the point, browser
|
||||
for outward-facing writes where being genuinely abang's session is the point. Or
|
||||
the reverse. Either way it is two lanes with one queue in front, not a migration.
|
||||
|
||||
## 🔴 DECIDED 2026-09-12 04:06 — both repos are OUT
|
||||
|
||||
Abang's call: **skip `twikit` and `aiograpi` entirely.** They log in via app
|
||||
requests, and the ban risk lands on his six real accounts. The house lane does
|
||||
not hold a password and does not need to. Kept in this napkin as the road not
|
||||
taken, not as a backlog item.
|
||||
|
||||
The section below is what replaced them — and it is better than either.
|
||||
|
||||
---
|
||||
|
||||
# The third lane — structured JSON, no DOM, no password
|
||||
|
||||
**This is the finding of the night.** Measured 2026-09-12 04:05-04:09 against
|
||||
abang's live Brave. The question was "do we still need DOM" — the answer is no,
|
||||
on all three platforms tested.
|
||||
|
||||
The principle: the page's own app already fetches structured JSON to render
|
||||
itself. **Read that, instead of reading the pixels it painted.** No credentials,
|
||||
no unofficial client, no new IP — the traffic is genuinely abang's, we only
|
||||
listen.
|
||||
|
||||
## One principle, two mechanisms
|
||||
|
||||
| Platform | Mechanism | Result |
|
||||
|---|---|---|
|
||||
| X | network capture — `/i/api/graphql/…/HomeLatestTimeline` | **190 tweets in 0.9s** |
|
||||
| Threads | network capture — `/graphql/query` → `feedData` | 4 posts, 22.9KB body |
|
||||
| IG | **in-document** — `RelayPrefetchedStreamCache` → `xdt_api__v1__feed__timeline` | 3 posts from a 150KB inline `<script>` |
|
||||
|
||||
X and Threads must be *heard*. IG must be *read* — Meta prefetches the feed into
|
||||
the document, so nothing crosses the wire to capture.
|
||||
|
||||
## X — the same source, both ways
|
||||
|
||||
| | DOM scrape | Network capture |
|
||||
|---|---|---|
|
||||
| Tweets | 10 | **190** |
|
||||
| Wall | 11.1s | **0.9s** |
|
||||
| Author | `"Thariq @trq212 · 31m"` (display string) | `trq212` (real `screen_name`) |
|
||||
| Counts | strings parsed out of the DOM | ints — `L=427 RT=19 RE=45` |
|
||||
| Views | usually absent | **`V=17912`** |
|
||||
| Posted at | `<time datetime>` attribute | full `created_at` |
|
||||
|
||||
IG returns the same quality: `@raybanmeta ts=1788538432 L=76919 C=945`. Exact
|
||||
integers, not `"76.9K"` recovered from a rendered string.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Network.enable on the tab session
|
||||
→ listen for Network.responseReceived, filter the URL
|
||||
→ wait for Network.loadingFinished for that requestId
|
||||
→ Network.getResponseBody(requestId)
|
||||
```
|
||||
|
||||
🔴 **`responseReceived` fires before the body has arrived.** The first probe read
|
||||
`encodedDataLength` at that moment and reported 149-byte GraphQL responses for
|
||||
Instagram. Waiting for `loadingFinished` turned the same Threads request into
|
||||
**22,697 characters**. Sizing a body at `responseReceived` is reading a letter
|
||||
while it is still in the post.
|
||||
|
||||
## The three real limits
|
||||
|
||||
1. **Capture must be live.** `getResponseBody` reads the renderer's buffer. Once
|
||||
the tab navigates away the body is gone — there is no asking later. The
|
||||
listener has to be armed *before* the request fires.
|
||||
2. **We only get what the page asks for.** Want DMs? The page has to go to DMs.
|
||||
Navigation is still our job; this replaces scraping, not driving.
|
||||
3. **Writes are still unanswered.** Reading is solved. Posting either stays on
|
||||
the DOM, or we capture the auth headers off one intercepted request and replay
|
||||
`fetch()` from inside the page — no password, no DOM. **Untested. It does not
|
||||
work until it has been fired.**
|
||||
|
||||
## Not proven — do not repeat these as facts
|
||||
|
||||
- ⚠️ **IG's 3 posts were all Meta-owned accounts** (`raybanmeta`, `whatsapp`,
|
||||
`metaglasses`). That is the prefetched/suggested set, **not abang's following
|
||||
feed**. His real feed most likely arrives in a later fetch — not captured yet.
|
||||
- ⚠️ **Threads returned 4 posts against X's 190.** That is page one only; the rest
|
||||
comes from scroll-triggered fetches. The 190 is X being generous in one
|
||||
response, not a fair per-platform comparison.
|
||||
- TikTok and YouTube have not been tried on this lane at all.
|
||||
|
||||
## What this does to the plan
|
||||
|
||||
- **Step 1 (background pass)** — unchanged and still first. Nothing here works on
|
||||
a timer while it steals focus.
|
||||
- **Step 2 (SQLite)** — gets *better*. Exact integers and real epochs arrive for
|
||||
free, so analytics stops being a parsing problem. `first_seen` still solves
|
||||
Threads' missing clock for notifications.
|
||||
- **Block 05 (analytics)** — close to free now. Views and counts ride along.
|
||||
- **Block 03 (comments & DMs)** — possibly cheap without `aiograpi` after all,
|
||||
since the inbox pages fetch their own JSON. Worth one probe before assuming.
|
||||
|
||||
## Open, added tonight
|
||||
|
||||
- Which lane owns **writes**? DOM (proven, slow) or replayed `fetch()` with
|
||||
captured headers (fast, unproven, and the one that could post something wrong
|
||||
outward-facing)?
|
||||
- Can IG's **real** following feed be captured, or is the prefetch all a logged-in
|
||||
web session gets without scrolling?
|
||||
- Does the capture lane reach **DMs and comments**? One probe answers whether
|
||||
block 03 is cheap or expensive.
|
||||
- TikTok and YouTube on this lane — untested.
|
||||
- Where does capture live? A listener armed per call, or one long-lived recorder
|
||||
that watches abang's normal browsing and fills the DB as a side effect?
|
||||
|
||||
|
||||
+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 ───────────────────────────────────────────────────────────
|
||||
|
||||
+335
-51
@@ -1,18 +1,35 @@
|
||||
{
|
||||
"_note": "Local-mode data for dashboard.html. Same shape as the artifact db collections. Written 2026-09-11 ~10:05 from live socialmag probes: check_login, get_status, get_feed, get_notifications.",
|
||||
"_yt_caveat": "yt is really LOGGED IN (ytcfg.get('LOGGED_IN') === true, verified live). socialmag check_login reports false because it tests for '#avatar-btn', which never renders in a backgrounded tab — YouTube leaves the masthead in skeleton state. Recorded here as the measured truth, not as check_login's answer.",
|
||||
"platforms": {
|
||||
"threads": { "loggedIn": true, "checkedAt": "2026-09-11T10:00:00+08:00" },
|
||||
"fb": { "loggedIn": true, "checkedAt": "2026-09-11T10:00:00+08:00" },
|
||||
"ig": { "loggedIn": true, "checkedAt": "2026-09-11T10:00:00+08:00" },
|
||||
"yt": { "loggedIn": true, "checkedAt": "2026-09-11T10:05:00+08:00" },
|
||||
"tiktok": { "loggedIn": true, "checkedAt": "2026-09-11T10:00:00+08:00" },
|
||||
"twitter": { "loggedIn": true, "checkedAt": "2026-09-11T10:00:00+08:00" }
|
||||
"fb": {
|
||||
"checkedAt": "2026-09-13T01:53:10+0800",
|
||||
"loggedIn": true
|
||||
},
|
||||
"ig": {
|
||||
"checkedAt": "2026-09-13T01:53:10+0800",
|
||||
"loggedIn": true
|
||||
},
|
||||
"threads": {
|
||||
"checkedAt": "2026-09-13T01:53:08+0800",
|
||||
"loggedIn": true
|
||||
},
|
||||
"tiktok": {
|
||||
"checkedAt": "2026-09-12T04:18:44+0800",
|
||||
"loggedIn": true
|
||||
},
|
||||
"twitter": {
|
||||
"checkedAt": "2026-09-13T01:53:09+0800",
|
||||
"loggedIn": true
|
||||
},
|
||||
"yt": {
|
||||
"checkedAt": "2026-09-13T01:53:09+0800",
|
||||
"loggedIn": true
|
||||
}
|
||||
},
|
||||
"posts": [
|
||||
{
|
||||
"platform": "twitter",
|
||||
"text": "midnight oil burns brightest ✨ first automated post from the house 🏠",
|
||||
"text": "midnight oil burns brightest \u2728 first automated post from the house \ud83c\udfe0",
|
||||
"status": "ok",
|
||||
"detail": "twitter: posted",
|
||||
"ts": "2026-09-11T00:00:58+08:00",
|
||||
@@ -22,69 +39,336 @@
|
||||
"notifications": [
|
||||
{
|
||||
"platform": "threads",
|
||||
"text": "Your thread got over 50 views. 11h late night coding sessions with good music is peak vibes ngl 🎧✨",
|
||||
"author": null,
|
||||
"text": "Your thread got over 50 views. 2d late night coding sessions with good music is peak vibes ngl \ud83c\udfa7\u2728",
|
||||
"url": "https://www.threads.com/@beethovenkassim9917/post/DdG2Sz1FOZp",
|
||||
"ts": "2026-09-10T23:00:00+08:00",
|
||||
"_ts_note": "stamped here from the row's relative marker (11h) — get_notifications returns no timestamp"
|
||||
"firstSeen": "2026-09-12T04:17:47+0800",
|
||||
"timesSeen": 19,
|
||||
"ts": "2026-09-11T04:17:25+0800"
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"text": "You created a new Threads profile in your Meta Account. Review how to log in. 14h",
|
||||
"author": null,
|
||||
"text": "You created a new Threads profile in your Meta Account. Review how to log in. 2d",
|
||||
"url": "https://accountscenter.threads.com/settings-updates?settings_notif_id=2060336524603315&num_profiles=1",
|
||||
"ts": "2026-09-10T20:00:00+08:00"
|
||||
"firstSeen": "2026-09-12T04:17:47+0800",
|
||||
"timesSeen": 19,
|
||||
"ts": "2026-09-11T04:17:25+0800"
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"text": "threads 14h Your profile is public. You can manage your privacy in settings anytime.",
|
||||
"author": null,
|
||||
"text": "threads 2d Your profile is public. You can manage your privacy in settings anytime.",
|
||||
"url": "https://www.threads.com/settings/privacy",
|
||||
"ts": "2026-09-10T20:00:00+08:00"
|
||||
"firstSeen": "2026-09-12T04:17:47+0800",
|
||||
"timesSeen": 19,
|
||||
"ts": "2026-09-11T04:17:25+0800"
|
||||
}
|
||||
],
|
||||
"feed": [
|
||||
{
|
||||
"author": "Boris Cherny @bcherny",
|
||||
"text": "Every day, I get a lot of of emails and messages like this one. I try to respond to as many as I can.\n\nSharing my response below, for anyone else in a similar situation. What do you think?",
|
||||
"time": "2026-09-11T01:10:26.000Z",
|
||||
"url": "https://x.com/bcherny/status/2098217571153838124",
|
||||
"likes": "384",
|
||||
"retweets": "14",
|
||||
"replies": "40"
|
||||
"platform": "threads",
|
||||
"author": "shessoprecious_",
|
||||
"text": "Embrace your natural gifts and talents.",
|
||||
"url": "https://www.threads.com/@shessoprecious_/post/DdMbHtyIAAR",
|
||||
"firstSeen": "2026-09-13T01:23:43+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-13T00:33:00+0800",
|
||||
"likes": 8,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"author": "Boris Cherny @bcherny",
|
||||
"text": "Hey ████,\n\nI think there is room for both.\n\n1. Prototypes and other throw-away code can be treated as totally black box. If you're going to throw it away anyway, and if the blast radius of it breaking is low, it doesn't need to be perfect.\n2. Production code written by Claude",
|
||||
"time": "2026-09-11T01:10:27.000Z",
|
||||
"url": "https://x.com/bcherny/status/2098217573276131577",
|
||||
"likes": "260",
|
||||
"retweets": "13",
|
||||
"replies": "22"
|
||||
"platform": "ig",
|
||||
"author": "faizanarran",
|
||||
"text": "",
|
||||
"url": "https://www.instagram.com/p/DdMYdDxt5X4/",
|
||||
"firstSeen": "2026-09-13T01:23:43+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-13T00:11:03+0800",
|
||||
"likes": 182,
|
||||
"replies": 2
|
||||
},
|
||||
{
|
||||
"author": "Elon Musk @elonmusk",
|
||||
"text": "Grok @Bot summary of SpaceX CFO talk",
|
||||
"time": "2026-09-11T01:09:06.000Z",
|
||||
"url": "https://x.com/elonmusk/status/2098217236771295567",
|
||||
"likes": "1.3K",
|
||||
"retweets": "269",
|
||||
"replies": "352"
|
||||
"platform": "threads",
|
||||
"author": "moneygatha",
|
||||
"text": "Being \"relatable\" is a marketing tactic; being \"authentic\" is a lifestyle.",
|
||||
"url": "https://www.threads.com/@moneygatha/post/DdMXWBOClua",
|
||||
"firstSeen": "2026-09-13T00:23:39+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-13T00:00:00+0800",
|
||||
"likes": 3,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"author": "Boris Cherny @bcherny",
|
||||
"text": "wait what",
|
||||
"time": "2026-09-11T00:39:52.000Z",
|
||||
"url": "https://x.com/bcherny/status/2098209880310264048",
|
||||
"likes": "282",
|
||||
"retweets": "15",
|
||||
"replies": "19"
|
||||
"platform": "threads",
|
||||
"author": "lessbusyrifqi",
|
||||
"text": "A creator doesn't need a perfect system. \r\nThey need one they actually use.",
|
||||
"url": "https://www.threads.com/@lessbusyrifqi/post/DdMQem-nUIK",
|
||||
"firstSeen": "2026-09-12T23:23:41+0800",
|
||||
"timesSeen": 2,
|
||||
"ts": "2026-09-12T23:00:08+0800",
|
||||
"likes": 2,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"author": "Kevin O'Connor @KevinOConnor",
|
||||
"text": "New pod with @SawyerMerritt diving into the Tesla Cybercab: What we loved, what can improve, our FSD experiences, and what's next for @robotaxi.\n\nWatch the full conversation here on X or listen to The Kevin O'Connor Show on your favorite podcast app.",
|
||||
"time": "2026-09-10T20:37:00.000Z",
|
||||
"url": "https://x.com/KevinOConnor/status/2098148756902355336",
|
||||
"likes": "735",
|
||||
"retweets": "101",
|
||||
"replies": "74"
|
||||
"platform": "threads",
|
||||
"author": "heyellemills",
|
||||
"text": "Your new life requires a new standard.",
|
||||
"url": "https://www.threads.com/@heyellemills/post/DdMPyv0FfN7",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T22:54:04+0800",
|
||||
"likes": 0,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "hoshibond",
|
||||
"text": "Distribution can make or break a product",
|
||||
"url": "https://www.threads.com/@hoshibond/post/DdMOmzbk2dJ",
|
||||
"firstSeen": "2026-09-12T23:23:41+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T22:43:39+0800",
|
||||
"likes": 1,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "morph.studies",
|
||||
"text": "Motivation gets you started, discipline makes you unstoppable",
|
||||
"url": "https://www.instagram.com/p/DdMLsbTSf7U/",
|
||||
"firstSeen": "2026-09-13T00:23:39+0800",
|
||||
"timesSeen": 2,
|
||||
"ts": "2026-09-12T22:18:32+0800",
|
||||
"likes": 3,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "jomhackmy",
|
||||
"text": "Building community is about creating opportunities, not just making useless noise \ud83e\udd1e\ud83c\udffc",
|
||||
"url": "https://www.threads.com/@jomhackmy/post/DdMJ09iGqrp",
|
||||
"firstSeen": "2026-09-13T01:53:34+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T22:01:53+0800",
|
||||
"likes": 5,
|
||||
"replies": 2
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "hillshots_",
|
||||
"text": "The life i wanna live.\ud83d\udd4a\ud83c\udf43",
|
||||
"url": "https://www.instagram.com/p/DdMI0ZaiIPs/",
|
||||
"firstSeen": "2026-09-13T01:23:43+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T21:54:09+0800",
|
||||
"likes": 845,
|
||||
"replies": 23
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "aqmlqq",
|
||||
"text": "Hi. Blh tlg suggestkan AI tools to self learning Python from scratch? Thankyou",
|
||||
"url": "https://www.threads.com/@aqmlqq/post/DdMIjOjlPzG",
|
||||
"firstSeen": "2026-09-13T00:23:39+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T21:50:44+0800",
|
||||
"likes": 9,
|
||||
"replies": 7
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "hesahera",
|
||||
"text": "Winter and tangerine \ud83c\udf4a",
|
||||
"url": "https://www.instagram.com/p/DdMHJhSodGg/",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T21:39:16+0800",
|
||||
"likes": 2255,
|
||||
"replies": 7
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "getaipaycheck",
|
||||
"text": "I've used AI to build apps I could never have shipped alone. An idea plus persistence gets you a working product now.",
|
||||
"url": "https://www.threads.com/@getaipaycheck/post/DdMFynTDGWt",
|
||||
"firstSeen": "2026-09-12T22:53:36+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T21:27:40+0800",
|
||||
"likes": 0,
|
||||
"replies": 1
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "buildwithmahinn",
|
||||
"text": "Hey app founders, how do you stay consistent updating about your app?\n\nI swear I just blinked and 10 days went by lol.\n\nWould love to hear what works for you - and drop what you're building below \u2764\ufe0f \nLet\u2019s connect \ud83e\udd1d",
|
||||
"url": "https://www.threads.com/@buildwithmahinn/post/DdMEhA2k2jD",
|
||||
"firstSeen": "2026-09-13T01:53:34+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T21:15:28+0800",
|
||||
"likes": 6,
|
||||
"replies": 10
|
||||
},
|
||||
{
|
||||
"platform": "threads",
|
||||
"author": "chrisldyson",
|
||||
"text": "Some creators are so obsessed with being useful\u2026\n\nthey accidentally make themselves unnecessary.",
|
||||
"url": "https://www.threads.com/@chrisldyson/post/DdMCvsylZlf",
|
||||
"firstSeen": "2026-09-12T22:23:33+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T21:00:02+0800",
|
||||
"likes": 1,
|
||||
"replies": 0
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "meitime__",
|
||||
"text": "A trip through the Kiso Valley \ud83c\udf3f\ud83d\ude83\nAvailable now on the Mei Time YouTube channel.\n\n\ud83d\udccdNarai-juku, Tsumago-juku\u00a0\n\n#nakasendo #naraijuku #tsumagojuku",
|
||||
"url": "https://www.instagram.com/p/DdL2xPNphw3/",
|
||||
"firstSeen": "2026-09-12T21:23:32+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T19:20:35+0800",
|
||||
"likes": 580,
|
||||
"replies": 9
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "faizanarran",
|
||||
"text": "",
|
||||
"url": "https://www.instagram.com/p/DdLvhIwRk43/",
|
||||
"firstSeen": "2026-09-12T20:23:30+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T18:13:09+0800",
|
||||
"likes": 293,
|
||||
"replies": 2
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "elonmusk",
|
||||
"text": "RT @poteto: Next week, @mattyp @roshan_s and myself will be speedrunning building a new company using only Grok @Bot on the Grok Bot Galaxy\u2026",
|
||||
"url": "https://x.com/elonmusk/status/2098821446009081945",
|
||||
"firstSeen": "2026-09-13T01:23:43+0800",
|
||||
"timesSeen": 2,
|
||||
"ts": "2026-09-12T17:10:01+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 125,
|
||||
"views": 441455
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "faizanarran",
|
||||
"text": "",
|
||||
"url": "https://www.instagram.com/p/DdLmdouRPHk/",
|
||||
"firstSeen": "2026-09-12T18:23:46+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T16:53:28+0800",
|
||||
"likes": 419,
|
||||
"replies": 2
|
||||
},
|
||||
{
|
||||
"platform": "ig",
|
||||
"author": "arcewane",
|
||||
"text": "Selling with Arcewane!\n\n#arabic #villa #luxurylifestyle #mansion #luxuryhomes",
|
||||
"url": "https://www.instagram.com/p/DdLmI87xAIG/",
|
||||
"firstSeen": "2026-09-12T22:23:33+0800",
|
||||
"timesSeen": 1,
|
||||
"ts": "2026-09-12T16:50:04+0800",
|
||||
"likes": 402,
|
||||
"replies": 2
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "poteto",
|
||||
"text": "Next week, @mattyp @roshan_s and myself will be speedrunning building a new company using only Grok @Bot on the Grok Bot Galaxy livestream! We\u2019ve been given just 3 days to do it, or else we might be turned into pizza \ud83e\udd2a it will be so fun and I hope to see you on the livestream!",
|
||||
"url": "https://x.com/poteto/status/2098815981787414978",
|
||||
"firstSeen": "2026-09-13T01:23:43+0800",
|
||||
"timesSeen": 2,
|
||||
"ts": "2026-09-12T16:48:18+00:00",
|
||||
"likes": 1273,
|
||||
"replies": 141,
|
||||
"reposts": 125,
|
||||
"views": 149016
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "ylecun",
|
||||
"text": "RT @stevesi: A person with mid single digit years of industry experience and months at one company and negligible external footprint quits\u2026",
|
||||
"url": "https://x.com/ylecun/status/2098815744590922105",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T16:47:22+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 179,
|
||||
"views": 3010
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "ylecun",
|
||||
"text": "RT @Dan_Jeffries1: If we're doomed to get the Oversight Committee for Pacing Economic Innovation and Development, we need to make sure it's\u2026",
|
||||
"url": "https://x.com/ylecun/status/2098815587946319892",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T16:46:44+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 12,
|
||||
"views": 2507
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "ylecun",
|
||||
"text": "RT @kevinnbass: The Coxon psyop was a decade and more than a billion dollars in the making.\n\nHere's the story.\n\nCoefficient Giving (formerl\u2026",
|
||||
"url": "https://x.com/ylecun/status/2098815511127560248",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T16:46:26+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 840,
|
||||
"views": 2417
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "ylecun",
|
||||
"text": "RT @PessimistsArc: It\u2019s never been more important and urgent to study and understand the history of technology panics, the power dynamics a\u2026",
|
||||
"url": "https://x.com/ylecun/status/2098814655745359894",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T16:43:02+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 15,
|
||||
"views": 2777
|
||||
},
|
||||
{
|
||||
"platform": "twitter",
|
||||
"author": "ylecun",
|
||||
"text": "RT @RichardSSutton: There are a lot of things wrong with this world\u2026\n\nbut too much intelligence is not one of them.",
|
||||
"url": "https://x.com/ylecun/status/2098814617673761230",
|
||||
"firstSeen": "2026-09-13T00:53:35+0800",
|
||||
"timesSeen": 3,
|
||||
"ts": "2026-09-12T16:42:53+00:00",
|
||||
"likes": 0,
|
||||
"replies": 0,
|
||||
"reposts": 232,
|
||||
"views": 2454
|
||||
}
|
||||
]
|
||||
],
|
||||
"_refreshed": {
|
||||
"at": "2026-09-13T01:53:34+0800",
|
||||
"by": "socialmag.refresher",
|
||||
"cdp": "http://127.0.0.1:9336",
|
||||
"browserReachable": true,
|
||||
"store": {
|
||||
"checks": 248,
|
||||
"items": 431,
|
||||
"metrics": 8386,
|
||||
"kinds": {
|
||||
"feed": 428,
|
||||
"notification": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=socialmag dashboard file server (:8321)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/mnt/playground/Playground_V2
|
||||
# No --bind, matching how this was started by hand: the page is reachable from
|
||||
# abang's LAN, not only from this box. openbridge stays loopback-only, so a LAN
|
||||
# viewer just falls back to opening a new tab — progressive enhancement, and the
|
||||
# page is never worse than it was.
|
||||
ExecStart=/usr/bin/python3 -m http.server 8321
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=socialmag openbridge (:8322) — open_link over localhost HTTP
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/mnt/playground/Playground_V2
|
||||
Environment=PYTHONPATH=/mnt/playground/Playground_V2
|
||||
Environment=SOCIALMAG_CDP_ENDPOINT=http://127.0.0.1:9336
|
||||
ExecStart=/usr/bin/python3 -m socialmag.openbridge
|
||||
# openbridge refuses to move to another port if :8322 is taken — no silent
|
||||
# fallback — so a clash fails loudly here instead of listening somewhere nobody
|
||||
# is looking.
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=socialmag refresh — login states, Threads activity, X timeline capture
|
||||
# Brave is not a service on this box. There is nothing to order against, so a
|
||||
# pass that finds no CDP records the third state and exits 0.
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
WorkingDirectory=/mnt/playground/Playground_V2
|
||||
Environment=PYTHONPATH=/mnt/playground/Playground_V2
|
||||
Environment=SOCIALMAG_CDP_ENDPOINT=http://127.0.0.1:9336
|
||||
# --skip-foreground is the whole reason this can run on a desktop abang is using.
|
||||
# Every other platform reads a JS-state signal in a hidden tab; tiktok still
|
||||
# activates, so it is left out and keeps its last stored result with its own
|
||||
# older checkedAt. Stale is honest. Drop the flag for a full pass by hand.
|
||||
ExecStart=/usr/bin/python3 -m socialmag.refresher --skip-foreground
|
||||
# No Restart=: a down browser is a waiting state the refresher records and exits
|
||||
# 0 for. The timer brings the next attempt, not a crash loop.
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=socialmag refresh every 30 minutes
|
||||
|
||||
[Timer]
|
||||
# 2 min after boot so Brave has a chance to be up; a pass that finds no browser
|
||||
# still records the unknown state rather than skipping.
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=30min
|
||||
# A pass takes ~28s with --skip-foreground. 30min is the desktop number, not
|
||||
# kdmirah's 10min: this box has a human at it, and the metrics table only needs
|
||||
# enough resolution to watch engagement move, not to watch it live.
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user