Commit Graph
11 Commits
Author SHA1 Message Date
juxtapoandClaude Opus 5 9968ba040f v0.7.4: the dash unit walked into the selectors.py trap
socialmag-dash crash-looped on a real box. WorkingDirectory was the repo and
ExecStart was `python -m http.server`, so socketserver's `import selectors`
found the repo's selectors.py and the unit died with "module 'selectors' has no
attribute 'SelectSelector'" every 5 seconds. :8321 never bound.

This is the SAME root cause already fixed twice this evening — once for
`-m socialmag` (console scripts, cli.py) and once for `-m venv` (run python
from /). Both fixes were about keeping the repo off sys.path, and then the unit
file put it right back on. A `-m` anywhere near this directory is the hazard,
not any particular entry point.

The dash now serves with --directory and sits in / instead of standing in the
tree it serves. The by-hand fallback instructions carried the identical bug and
now do the same, with a line saying why.

openbridge and refresh were never affected: they exec console scripts, which do
not put the working directory on sys.path. That is exactly why cli.py exists.

The real fix is renaming selectors.py and queue.py. Until then this trap keeps
finding new ways in, and that is not a prediction — it is three for three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 02:46:06 +08:00
juxtapoandClaude Opus 5 9dfee100d2 v0.7.3: work out the user bus instead of asking for it by hand
A shell entered via `su` keeps the ORIGINAL user's XDG_RUNTIME_DIR, so
`systemctl --user` hunts for the bus in someone else's directory and finds
nothing — even when this user's manager is running perfectly. v0.7.2 detected
that and told the operator to export the right path. Typing /run/user/100
instead of /run/user/1000 cost a whole run.

The runtime dir is /run/user/<uid> by definition. The script now derives it,
retries the bus check with it, and installs the units — then tells the operator
to export it for the REST of their shell, because a child process cannot fix
its parent's environment.

The failure message also stopped guessing. It now distinguishes the two real
cases: /run/user/<uid> missing (no logind session — enable-linger, and no fresh
login is needed, contrary to what v0.7.1 said) versus present but unanswered
(the manager itself is the problem — go look at user@<uid>.service).

Four branches fired in a sandbox with three stub systemctls: bus answers
directly; bus answers only with the corrected dir; runtime dir absent; runtime
dir present but manager silent. All exit 0, and the middle one installs units
that the previous version would have skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 02:45:04 +08:00
juxtapoandClaude Opus 5 b928ac54df v0.7.2: check ensurepip, not import venv
Found by running install.sh on a second box, not by rereading it. Debian ships
the venv module but splits ensurepip into python3.N-venv. `python3 -c "import
venv"` therefore SUCCEEDS on a machine where `python3 -m venv` cannot work —
the pre-flight passed and the install died two steps later, after writing a
half-built .venv.

Importing a module says nothing about whether it can do its job. The check is
now `import ensurepip`, it runs before anything touches disk, and the error
names the exact versioned package (python3.13-venv on that box).

The old failure text also offered a two-branch diagnosis that matched neither
the real cause nor Python's own message, which already named the right package.
Guessing on top of a correct error makes it worse. It now says where to look
and gets out of the way.

Fired both ways in a sandbox: with ensurepip the install completes; with a
python stubbed to refuse it, the script dies early, prints the versioned apt
package, and leaves no .venv behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 02:34:11 +08:00
juxtapoandClaude Opus 5 c21399bd61 v0.7.1: install.sh survives boxes this one's assumptions did not fit
Two portability bugs, both found by pre-flighting a second machine (kdmirah,
a Debian LXC) rather than by reading the script again.

1. `systemctl` existing does not mean a user bus is reachable. Inside an LXC,
   over `su`, or with no logind session, `systemctl --user` fails with
   "Failed to connect to user scope bus". The old check asked whether the
   binary was on PATH, then ran `systemctl --user daemon-reload` under
   `set -euo pipefail` — so the install ABORTED halfway, after the venv and
   before the hermes wiring. It now asks the real question,
   `systemctl --user show-environment`, and on failure installs no units, says
   why (naming XDG_RUNTIME_DIR and the usual causes), prints the three
   commands to run by hand, and continues to hermes.

2. The hermes interpreter was hardcoded to this box's layout,
   /usr/local/lib/hermes-agent/venv/bin/python. kdmirah installs hermes under
   ~/.local/bin with its venv at ~/.hermes/hermes-agent/venv. Both are bash
   wrappers that exec their python, so the path is now read out of the wrapper
   (with a pip console-script shebang as the second shape). Verified against
   both machines: each derives its own interpreter and each reports mcp present.

Also: the closing summary no longer prints the dashboard URL and
`journalctl --user -u socialmag-refresh` when no units were installed. Pointing
someone at a journal for a unit that was never created is the same class of lie
as a silent fallback — it reads as working and answers with nothing.

Both branches fired in a sandbox with a fake HOME and two stub systemctls, one
that answers show-environment and one that refuses it. Both exit 0; the
refusing one installs nothing, warns loudly, and still registers hermes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 02:32:13 +08:00
juxtapoandClaude Opus 5 01fae3ca74 v0.7: one-click install — pyproject, console scripts, user units, hermes wiring
A clone + ./install.sh + ./launch-brave.sh + sign in once. Nothing else.

pyproject.toml declares the one real dependency (websockets) and maps the
repo-root-is-the-package layout, which retires the PYTHONPATH=<parent> line
every unit used to carry.

cli.py backs three console scripts. They exist because `python3 -m socialmag`
run from INSIDE a clone dies at `import asyncio`: the repo root is the package
dir, so it ships selectors.py and queue.py, and python puts the current
directory first on sys.path. `cd socialmag` is the first thing anyone does
after a clone, so the documented workaround failed at exactly the moment it
would be tried. A generated bin/ script never puts cwd on sys.path.

The same trap bit `python3 -m venv` inside install.sh — the installer was
broken by the thing it exists to fix. Every python3 call now runs from /.

deploy/user/*.in replaces root system units with systemd --user templates:
no User=, WantedBy=default.target, venv paths, @REPO@/@VENV@/@CDP_ENDPOINT@
substituted at install time. deploy/home/ is untouched and still live here.

launch-brave.sh carries --disable-background-timer-throttling. That is
load-bearing, not tidiness: five of six platforms are read in a hidden tab, and
a throttled background tab stops running the timers those pages use to fetch
their own data — the reads come back empty with no error.

install.sh checks the mcp package in hermes's own interpreter and refuses to
continue without it. Hermes treats mcp as optional and silently no-ops the
whole subsystem when it is missing, so a server that never loaded looks exactly
like one that loaded and did nothing.

It will not merge into an existing mcp_servers: key — it backs up config.yaml
and appends only when the key is absent, otherwise it prints the block. Blind
YAML merging is how someone's other servers disappear.

Verified end-to-end in a sandbox with a fake HOME and stubbed systemctl:
real MCP initialize handshake from inside the repo dir returns all 7 tools;
hermes's own yaml parser re-reads the written config with every pre-existing
key intact; a second run leaves the mcp_servers key count at 1; launch-brave.sh
exits 1 against a busy port; no unsubstituted @PLACEHOLDER@ in any rendered unit.

Not fixed, pre-existing: server.py reports SERVER_VERSION 0.1.0 while the repo
is at v0.7. The handshake shows 0.1.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 02:10:58 +08:00
juxtapoandClaude Opus 5 15f3388e5e 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>
2026-09-13 01:58:45 +08:00
juxtapoandClaude Opus 5 22e63ebf35 v0.5: capture lane, sqlite store, refresher wired to both
capture.py — read the platform's own JSON instead of the pixels it painted.
Network capture on X (/i/api/graphql HomeLatestTimeline) and Threads
(/graphql/query feedData); in-document read on Instagram, whose feed is
prefetched into a RelayPrefetchedStreamCache script and never crosses the
wire. 190 X rows in 0.9s against the DOM lane's 10 in 11.1s, with exact
integer counts and view numbers the DOM never exposes. Waits for
Network.loadingFinished, not responseReceived — the body is not there yet at
responseReceived. Nothing captured raises; an empty list would say the
platform posted nothing when the truth is our listener missed it.

store.py — SQLite, stdlib only. platform_checks / items / metrics.
id = sha1(platform|kind|url or text), first_seen written once and ts under
COALESCE so a derived Threads marker cannot decay across passes.
FEED_PER_PLATFORM = 10 caps any one platform before the merge, or X's ~185
rows a pass sort Instagram's 3 off the page entirely. dashboard.json is now a
derived view of the database; dashboard.html did not have to change for it.

refresher.py — check_all runs the background-safe platforms in parallel
(19.0s sequential to 6.1s), tiktok still sequential and foreground.
--skip-foreground leaves it out of a timed pass and store.latest_checks keeps
returning its last real result with its own older checkedAt; stale is honest,
a fabricated row is not. Browser down stays the third state and exits 0.

.gitignore: socialmag.db (regenerable), deploy/kdmirah/ (carries the box's
VNC password and addresses — stays in the lab, out of git).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 01:58:36 +08:00
juxtapoandClaude Opus 5 5419b08264 v0.4: background reads, JS-state login signals, blind sleeps dropped
get_or_open_tab(activate=True) — added, not replaced. Read paths pass
activate=False; post flows keep the foreground for dialogs and file inputs.

check_login moved off DOM signals onto session state: fb c_user, ig/threads
ds_user_id, twitter twid, yt ytcfg.LOGGED_IN. Three platforms were reporting
OUT on a signed-in account because their composer never mounts in a hidden
tab. The fixed sleep(4)/sleep(5) before each check goes with them — every
signal reads present the moment navigate() returns.

Unreadable cookies raise instead of returning False: "cannot tell" is not
"signed out". tiktok untouched and still activated — it reported
visibilityState "visible" from a sibling-activated tab, so it has never been
read from a genuine background.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 01:58:25 +08:00
juxtapo 1ac06c1bbe v0.3: dashboard local mode, openbridge tab reuse, tab activation fix
- dashboard.html: local dashboard.json mode (10s poll) alongside artifact db mode;
  clickable cards behind an http(s)-only safeUrl filter; meta charset; mode-aware
  freshness line
- openbridge.py: localhost shim exposing open_link so dashboard clicks reuse the
  platform's warm tab instead of spawning one per click
- cdp.py: Tab.activate(); get_or_open_tab activates reused tabs — a backgrounded
  tab never finishes rendering, which made YouTube's login check read a signed-in
  account as signed out
- README/MANIFEST: corrected CDP port (9336, was documented as 9222), removed the
  false claim that the server writes dashboard.json, documented the reboot/VPS
  requirements
- .gitignore: __pycache__/, *.pyc, state.json, Archived/; removed stale tracked
  pycache and state.json from index
2026-09-11 10:18:38 +08:00
juxtapo eaedf4f409 v0.2: twitter module, feed/notifications/reply/open_link tools, dashboard 2026-09-11 02:20:13 +08:00
juxtapo a4f33f456e socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok) 2026-09-10 21:10:55 +08:00