mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Compare commits
367
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff107fa556 | ||
|
|
bc5cb631e0 | ||
|
|
a2dbaccc6c | ||
|
|
732a781104 | ||
|
|
0bc9c722a5 | ||
|
|
f7d0a458cd | ||
|
|
1b1c7ff373 | ||
|
|
5cf53824c0 | ||
|
|
af8e654010 | ||
|
|
23c69eba0f | ||
|
|
36716dfe1e | ||
|
|
3104843276 | ||
|
|
5b0adf6b3e | ||
|
|
aff8841694 | ||
|
|
f01ce01259 | ||
|
|
2f9ac1e553 | ||
|
|
7594c54deb | ||
|
|
d5a69faaf0 | ||
|
|
af61c2716b | ||
|
|
eafa364d39 | ||
|
|
e35cb80ca1 | ||
|
|
2754a8af1f | ||
|
|
fd31797bd5 | ||
|
|
c20635bc85 | ||
|
|
1bf50c1f7a | ||
|
|
1b2362d234 | ||
|
|
04c8c7cf61 | ||
|
|
e266b00d79 | ||
|
|
b908803232 | ||
|
|
2d695e7acd | ||
|
|
ab1ea6dee1 | ||
|
|
e9af76dfd2 | ||
|
|
5f8e58bec0 | ||
|
|
0ffd92a8d2 | ||
|
|
a94530403d | ||
|
|
49c5839115 | ||
|
|
7c878d65f8 | ||
|
|
75e00f2560 | ||
|
|
b149cebf31 | ||
|
|
83a847e059 | ||
|
|
146076dd38 | ||
|
|
14cd7b7467 | ||
|
|
d1d6e0fed1 | ||
|
|
1222805b2d | ||
|
|
7f4113ad54 | ||
|
|
7b667e797a | ||
|
|
a482d9d20f | ||
|
|
103eb04cd9 | ||
|
|
492557400c | ||
|
|
144ff557b1 | ||
|
|
83fd2495b4 | ||
|
|
280f043a66 | ||
|
|
dd092ed856 | ||
|
|
3a89abd05b | ||
|
|
2955645297 | ||
|
|
6c64b97156 | ||
|
|
d7184f07bd | ||
|
|
672ca95395 | ||
|
|
98a20b991d | ||
|
|
b38cefca36 | ||
|
|
79a9a1301c | ||
|
|
cdc398b028 | ||
|
|
8317da4d84 | ||
|
|
265ce4261f | ||
|
|
d9424fb7bd | ||
|
|
fb0e671d4d | ||
|
|
7edc7ea86f | ||
|
|
888807baf6 | ||
|
|
d04d501ee2 | ||
|
|
4f1d954872 | ||
|
|
77f03cd6c6 | ||
|
|
0219bd31a4 | ||
|
|
b21ce86c8e | ||
|
|
f41ac656f6 | ||
|
|
09f52bdd50 | ||
|
|
a79d49df42 | ||
|
|
26e9a787cf | ||
|
|
64299297e1 | ||
|
|
d917053daa | ||
|
|
75bc92f3a4 | ||
|
|
69afc2f1a4 | ||
|
|
a797d9a8f1 | ||
|
|
b8e87e655b | ||
|
|
6495e0618c | ||
|
|
30a13b2bbf | ||
|
|
63e2b51961 | ||
|
|
984c219cd2 | ||
|
|
aee09d14e1 | ||
|
|
34edc6ce65 | ||
|
|
4bea541e67 | ||
|
|
e887ceafda | ||
|
|
e9c960edcf | ||
|
|
4de357e4a7 | ||
|
|
48999ca22e | ||
|
|
f4618d5d29 | ||
|
|
ed3fff12d6 | ||
|
|
5806fa69cb | ||
|
|
89f8923c6e | ||
|
|
fb1997de42 | ||
|
|
ed5fb64991 | ||
|
|
e21be27f34 | ||
|
|
145c09f7a6 | ||
|
|
56417d11f0 | ||
|
|
e04549f816 | ||
|
|
89c5494aaa | ||
|
|
67140d891f | ||
|
|
169d894f72 | ||
|
|
076ec80295 | ||
|
|
7369f1feeb | ||
|
|
f9bc7cb639 | ||
|
|
c5b678f7c9 | ||
|
|
c6022f21c6 | ||
|
|
fb54cea3a2 | ||
|
|
ee7391b4d2 | ||
|
|
63a55e66e5 | ||
|
|
3a7730d5e8 | ||
|
|
9cbb53f821 | ||
|
|
f233eaf2b3 | ||
|
|
2ea6f0682b | ||
|
|
cfc20107bb | ||
|
|
c03ad2c003 | ||
|
|
892c7f5256 | ||
|
|
43cb1bafe6 | ||
|
|
3474668a83 | ||
|
|
3dac0afb8d | ||
|
|
488d874a97 | ||
|
|
12f6d1d0b0 | ||
|
|
8b04a69fb4 | ||
|
|
5b77fd2029 | ||
|
|
c8e07cc508 | ||
|
|
6609269ea1 | ||
|
|
15d2fe996a | ||
|
|
e7a103a8fa | ||
|
|
7813070216 | ||
|
|
0ed41bb66f | ||
|
|
9b8ec5539c | ||
|
|
3fd0ab594d | ||
|
|
f3dc7d1355 | ||
|
|
c017069cc0 | ||
|
|
98cde500b6 | ||
|
|
60707d2a8a | ||
|
|
06e920e77e | ||
|
|
17163984f7 | ||
|
|
de81198a68 | ||
|
|
09b132191a | ||
|
|
7e8ef49dca | ||
|
|
cc72ea37ba | ||
|
|
b117416355 | ||
|
|
f39622da3b | ||
|
|
fecf42948f | ||
|
|
d7d632f88b | ||
|
|
092d9c7046 | ||
|
|
d07f471454 | ||
|
|
e661e6a3b1 | ||
|
|
48ceb2805c | ||
|
|
4e90434992 | ||
|
|
4bda9aab44 | ||
|
|
4a1d182653 | ||
|
|
16cc80e1eb | ||
|
|
386cc60e1f | ||
|
|
36daa5257e | ||
|
|
b2564bc977 | ||
|
|
fa358eb4c1 | ||
|
|
4b74da7372 | ||
|
|
b3fab0326b | ||
|
|
ec41d3381c | ||
|
|
ee588ba6a1 | ||
|
|
c5818cfe67 | ||
|
|
aacb0d147b | ||
|
|
d9b0cb510e | ||
|
|
1b3ef49cbb | ||
|
|
c4aac993d7 | ||
|
|
4900fa95f7 | ||
|
|
d9aef54d3b | ||
|
|
a7e6687f00 | ||
|
|
25942a156c | ||
|
|
612ad83f9d | ||
|
|
e7b9afb309 | ||
|
|
1650b3439f | ||
|
|
4c386631b9 | ||
|
|
9afe48a5b5 | ||
|
|
8d7e60ebf1 | ||
|
|
020f0577ee | ||
|
|
7b8100a6a1 | ||
|
|
0535b3dbfc | ||
|
|
8abbaa77b4 | ||
|
|
ff89f000bb | ||
|
|
5de86e7578 | ||
|
|
ac99ae9a6b | ||
|
|
cae1c5321b | ||
|
|
47176efe1b | ||
|
|
8356f27d48 | ||
|
|
bbf57388cb | ||
|
|
7030d73259 | ||
|
|
308ddd5a46 | ||
|
|
3f9c26e361 | ||
|
|
e8fd120afa | ||
|
|
291e653839 | ||
|
|
402500d3d2 | ||
|
|
119ba62934 | ||
|
|
1bb3a4fc45 | ||
|
|
64143f42ed | ||
|
|
d88716e3c6 | ||
|
|
e1ce387059 | ||
|
|
124be14310 | ||
|
|
7f99c04569 | ||
|
|
032f76bcdb | ||
|
|
6cf426130f | ||
|
|
d3f4c2155f | ||
|
|
699d72927b | ||
|
|
fc259786af | ||
|
|
4a3aee9e0b | ||
|
|
1320bedac7 | ||
|
|
dc8525973a | ||
|
|
bfa36a3f35 | ||
|
|
2515681216 | ||
|
|
62ed17e29c | ||
|
|
0bd0b1c3e3 | ||
|
|
4bc52f9ded | ||
|
|
6944d6f9df | ||
|
|
95a77a7fd7 | ||
|
|
e9d8886c3a | ||
|
|
acb2fdef3b | ||
|
|
025771a84d | ||
|
|
0919be930c | ||
|
|
c82ce8759e | ||
|
|
4b8c074698 | ||
|
|
f2a392fa3b | ||
|
|
2fb6a53656 | ||
|
|
b922c18601 | ||
|
|
ff66ebf55a | ||
|
|
2a5ba463c4 | ||
|
|
4c7a897ce3 | ||
|
|
371e79027a | ||
|
|
12f927321f | ||
|
|
f3c0ffd3ba | ||
|
|
140618f88f | ||
|
|
bbc1be79e9 | ||
|
|
47953970ec | ||
|
|
0c1dd7e86b | ||
|
|
8c80ae2027 | ||
|
|
3f627f5df3 | ||
|
|
320a992249 | ||
|
|
aef4501c4c | ||
|
|
f116f7c838 | ||
|
|
94676453fe | ||
|
|
374a8aff91 | ||
|
|
89ccb907c9 | ||
|
|
7055496b30 | ||
|
|
ce1381f500 | ||
|
|
d9bd2df6e8 | ||
|
|
77d486fb56 | ||
|
|
84bb183ab9 | ||
|
|
50a791f486 | ||
|
|
434d6a9559 | ||
|
|
4c54858c98 | ||
|
|
967448caea | ||
|
|
604c8c61f4 | ||
|
|
fe3a6a83af | ||
|
|
6cd77767e1 | ||
|
|
c206e0a3c0 | ||
|
|
6d4ed9200c | ||
|
|
7fde734d87 | ||
|
|
1c49eb3b2c | ||
|
|
b815446a52 | ||
|
|
7d20a509ca | ||
|
|
e522efc191 | ||
|
|
607d5ce1a1 | ||
|
|
712b05cf87 | ||
|
|
66832dc3a9 | ||
|
|
03766d39ef | ||
|
|
f9adfc8f53 | ||
|
|
af16ee1d09 | ||
|
|
cee4962166 | ||
|
|
de6918cee8 | ||
|
|
683cedf0bf | ||
|
|
181eae95d9 | ||
|
|
3968a97922 | ||
|
|
3770eb4186 | ||
|
|
4a6978ebc4 | ||
|
|
16f6dca41e | ||
|
|
6da037f59b | ||
|
|
dbdac18df1 | ||
|
|
d02240786d | ||
|
|
be6058b0af | ||
|
|
c3fee19059 | ||
|
|
e106a15ea5 | ||
|
|
f303ab0e72 | ||
|
|
9e6c310d80 | ||
|
|
c312399117 | ||
|
|
3514c0570d | ||
|
|
3e451aac0b | ||
|
|
8b204687d2 | ||
|
|
fa538097bd | ||
|
|
386cf14bbe | ||
|
|
d90591163f | ||
|
|
8d4622bb7d | ||
|
|
5fdf53deb8 | ||
|
|
4dd3eb0330 | ||
|
|
911afb1c05 | ||
|
|
6eb89afcec | ||
|
|
c098612c94 | ||
|
|
1ddfa95a84 | ||
|
|
fbac54dd48 | ||
|
|
58922e55c3 | ||
|
|
5e69fa4a7d | ||
|
|
8d40d85079 | ||
|
|
847a855ec8 | ||
|
|
5cce3b726a | ||
|
|
975b996f4c | ||
|
|
2eb7d09865 | ||
|
|
e99ba0e817 | ||
|
|
ab120a03e1 | ||
|
|
5d0709f5f4 | ||
|
|
350feae907 | ||
|
|
31be84ba82 | ||
|
|
f48150ed6b | ||
|
|
244f527cf6 | ||
|
|
99f8e88969 | ||
|
|
c0d006a5fc | ||
|
|
04075ff56b | ||
|
|
e5784893a8 | ||
|
|
d141c316a9 | ||
|
|
b84558e928 | ||
|
|
38b51f13a5 | ||
|
|
4404728410 | ||
|
|
726c0d26ed | ||
|
|
bb6614e3d3 | ||
|
|
687f8c438d | ||
|
|
2c4aefe872 | ||
|
|
b774de6d76 | ||
|
|
5f439d6863 | ||
|
|
fe976b3ab7 | ||
|
|
4525c1c657 | ||
|
|
3735d57410 | ||
|
|
4dd150b010 | ||
|
|
c200c84fc5 | ||
|
|
d19040c625 | ||
|
|
5764482f2a | ||
|
|
4b975c17dc | ||
|
|
87b3012e34 | ||
|
|
e8e2b04de7 | ||
|
|
2f3028f2e4 | ||
|
|
f0a3ede3e5 | ||
|
|
d8e5113efd | ||
|
|
04c8511a50 | ||
|
|
ce14004725 | ||
|
|
4458b269be | ||
|
|
f373bb9ce7 | ||
|
|
2cda5025d4 | ||
|
|
7739a934f9 | ||
|
|
98c623c85a | ||
|
|
0513c7fcd8 | ||
|
|
855022e911 | ||
|
|
589bda8cb0 | ||
|
|
18e08bf2cb | ||
|
|
126a613527 | ||
|
|
4eaa78cf59 | ||
|
|
f49080558b | ||
|
|
f25c2ac242 | ||
|
|
4d1116530b | ||
|
|
f2d390ff18 | ||
|
|
ca7b711b8a | ||
|
|
88c906fd34 | ||
|
|
61f7d3f15e | ||
|
|
1d14a1f392 | ||
|
|
676844edfc |
No files matched your search
@@ -0,0 +1,33 @@
|
||||
# Repo Memory
|
||||
|
||||
Curated, reviewed knowledge for coding agents working in this repository.
|
||||
Memory is an accelerator, not a second source of truth.
|
||||
|
||||
## Precedence (highest first)
|
||||
|
||||
1. Source code and tests
|
||||
2. Executable configuration
|
||||
3. Accepted design records / docs
|
||||
4. `AGENTS.md`
|
||||
5. This memory
|
||||
|
||||
If memory conflicts with anything above it, the memory is stale — propose a fix.
|
||||
|
||||
## Topics
|
||||
|
||||
- `memory/architecture.md` — durable architectural decisions and constraints
|
||||
- `memory/conventions.md` — validated conventions not obvious from the code
|
||||
- `memory/lessons.md` — hard-won lessons and recurring landmines
|
||||
- `memory/decisions.jsonl` — structured decision records (one JSON object per line)
|
||||
|
||||
## Write policy
|
||||
|
||||
Agents MAY read everything here and MAY add candidates under `proposals/`
|
||||
(one file per proposal, named `YYYY-MM-DD-<slug>.md`, with evidence links).
|
||||
Agents MUST NOT edit `memory/` directly; trusted memory changes only through
|
||||
reviewed commits that promote a proposal.
|
||||
|
||||
## Prohibited content
|
||||
|
||||
Credentials or secrets, personal data, raw transcripts or tool logs, session
|
||||
TODOs, high-frequency agent state, unverified external claims.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Architecture
|
||||
|
||||
Durable architectural decisions and constraints for bitvid.
|
||||
|
||||
## AGENTS.md is a thin constitution; long-form standards live in docs/
|
||||
|
||||
On 2026-08-12 AGENTS.md was split into a thin agent constitution plus
|
||||
reference docs under `docs/testing/` and `docs/lessons/`, because the file had
|
||||
outgrown the 32 KiB harness context budget. AGENTS.md keeps only the invariant
|
||||
and a pointer per topic; new long-form material belongs in `docs/`, not in
|
||||
AGENTS.md.
|
||||
|
||||
Authoritative source:
|
||||
- docs/testing/test-integrity.md
|
||||
- docs/testing/agent-playwright-harness.md
|
||||
- docs/lessons/
|
||||
- git commit 911afb1c ("Split AGENTS.md reference sections into docs/; keep thin agent constitution")
|
||||
@@ -0,0 +1,5 @@
|
||||
# Conventions
|
||||
|
||||
Validated conventions not obvious from the code.
|
||||
|
||||
No entries yet.
|
||||
@@ -0,0 +1 @@
|
||||
{"kind":"decision","subject":"docs/agents-md-split","summary":"AGENTS.md was split on 2026-08-12 into a thin agent constitution plus reference docs under docs/testing/ and docs/lessons/ to stay within the 32 KiB harness context budget; AGENTS.md keeps only invariants and pointers, and new long-form material belongs in docs/.","status":"active","evidence":[{"type":"repository","ref":"docs/testing/test-integrity.md"},{"type":"repository","ref":"docs/testing/agent-playwright-harness.md"},{"type":"repository","ref":"docs/lessons/"},{"type":"git","ref":"911afb1c"}],"created":"2026-08-12","supersedes":[]}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Lessons
|
||||
|
||||
Hard-won lessons and recurring landmines.
|
||||
|
||||
## Cloudflare/S3 upload-path gotchas
|
||||
|
||||
`js/services/r2Service.js` and `js/services/s3UploadService.js` are near-identical
|
||||
copies that must be changed in lockstep, and storage keys must stay
|
||||
content-addressed (`computeStorageContentHash`) or URL-first uploads silently
|
||||
overwrite objects. Full audited writeup in the doc below.
|
||||
|
||||
Authoritative source:
|
||||
- docs/lessons/cloudflare-s3-upload-gotchas.md
|
||||
|
||||
## NIP-07 signer reliability
|
||||
|
||||
"DMs / lists / watch-history won't load after login" is almost always an
|
||||
unresponsive NIP-07 signer extension, not bitvid — diagnose with a raw
|
||||
`window.nostr` probe first, and never regress the five resilience invariants
|
||||
(capped relay fan-out, NIP-07 circuit breaker, re-thrown transient decrypt
|
||||
errors, generous decrypt budgets, lazy modal population).
|
||||
|
||||
Authoritative source:
|
||||
- docs/lessons/nip07-signer-reliability.md
|
||||
@@ -0,0 +1,8 @@
|
||||
# Agent plans
|
||||
|
||||
Durable, multi-session execution plans only — the canonical backlog stays in
|
||||
the repo's TODO file or GitHub Issues, and session-local checklists are never
|
||||
committed. One plan per task, named `<ticket-or-slug>.md`, in `active/`;
|
||||
move finished plans to `archive/`. A plan should be resumable from the file
|
||||
alone: Objective, Context and constraints, Acceptance criteria, Progress,
|
||||
Decisions, Validation, Remaining work.
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
# Memory proposals
|
||||
|
||||
Agents: drop candidate memories here, one file per proposal, named
|
||||
`YYYY-MM-DD-<slug>.md`. State the claim, the evidence (file paths, commits,
|
||||
test results), and why it is durable. A human reviews and either promotes it
|
||||
into `../memory/` in a reviewed commit or deletes it. Nothing in this
|
||||
directory is trusted context.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Repo Memory Decision Record",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["kind", "subject", "summary", "status", "evidence", "created"],
|
||||
"properties": {
|
||||
"kind": {"enum": ["decision", "constraint", "lesson", "observation", "workaround"]},
|
||||
"subject": {"type": "string", "minLength": 1},
|
||||
"summary": {"type": "string", "minLength": 1, "maxLength": 4096},
|
||||
"status": {"enum": ["proposed", "active", "superseded", "expired"]},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "ref"],
|
||||
"properties": {
|
||||
"type": {"enum": ["repository", "git", "issue", "test", "external"]},
|
||||
"ref": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
"created": {"type": "string"},
|
||||
"expires": {"type": ["string", "null"]},
|
||||
"supersedes": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
name: agent-fleet
|
||||
description: Use fleet-mcp tools for backlog, mailbox, landing queue, and area ownership. Do not use it to start sessions or push main.
|
||||
---
|
||||
|
||||
Follow the fleet rules in `AGENTS.md`. Call the `agent-fleet` MCP tools for backlog, mailbox, landing-queue, area questions, and commits. Editing those files directly is also fine. Sandboxed Codex cannot write Git's index: use the `commit` tool (or `fleet commit -m "..." --area <id> -- <paths>`), not `git commit`. `submit: true` queues the landing and starts a drain. Go back to work. At your next natural checkpoint, `land_status` (or `fleet landq status <id>`) can report that submission. Waiting is optional. Do not infer the lander or the land result from silence. Other providers may use either commit path.
|
||||
|
||||
If you installed this fleet, you stay on as the interpreter. Read `docs/agents/prompts/interpreter.md`. Run `fleet up` yourself. Talk to the session manager with the MCP tools `tell` and `hear`. Do not send those with the shell `fleet tell`: that is a sandboxed command, and the MCP approval mode does not apply to it. Do not scrape the tmux pane, and do not send the user to a terminal. A routine yes you relay counts. Money, pushes, deploys, deletes, secrets, outgoing messages, and a bigger structural or directional change go to the user. Uncommitted setup files use `setup_commit`, not `git commit`. Workers do not start the fleet. `fleet crontab` stays unused unless the user asks.
|
||||
|
||||
You may use this harness's own subagents for parallel research, investigation, test runs, or review. You coordinate edits in a worktree so subagents do not edit the same files at the same time. Landing still goes through your `commit` and the queue.
|
||||
|
||||
To update the kit: `git pull` in `~/agent-fleet-kit`, reinstall, run `fleet update`, and restart agents after handoff if it says so. Do not re-run init to upgrade.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Agent instruction and memory surfaces require maintainer review
|
||||
/AGENTS.md @PR0M3TH3AN
|
||||
/CLAUDE.md @PR0M3TH3AN
|
||||
/.agents/ @PR0M3TH3AN
|
||||
/SECURITY.md @PR0M3TH3AN
|
||||
/.github/workflows/agent-governance.yml @PR0M3TH3AN
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Agent governance
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "AGENTS.md"
|
||||
- "CLAUDE.md"
|
||||
- ".agents/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "AGENTS.md"
|
||||
- "CLAUDE.md"
|
||||
- ".agents/**"
|
||||
|
||||
jobs:
|
||||
lint-agent-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate agent artifacts
|
||||
run: |
|
||||
python3 - <<'EOF'
|
||||
import json, os, sys
|
||||
hard_fail = False
|
||||
|
||||
# decisions.jsonl: every line must be valid JSON with required keys
|
||||
path = ".agents/memory/decisions.jsonl"
|
||||
if os.path.exists(path):
|
||||
required = {"kind", "subject", "summary", "status", "evidence", "created"}
|
||||
for i, line in enumerate(open(path, encoding="utf-8"), 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"::error file={path},line={i}::invalid JSON: {e}")
|
||||
hard_fail = True
|
||||
continue
|
||||
missing = required - rec.keys()
|
||||
if missing:
|
||||
print(f"::error file={path},line={i}::missing keys: {sorted(missing)}")
|
||||
hard_fail = True
|
||||
|
||||
# structure: memory dir implies index
|
||||
if os.path.isdir(".agents") and not os.path.exists(".agents/MEMORY.md"):
|
||||
print("::error::.agents/ exists but .agents/MEMORY.md index is missing")
|
||||
hard_fail = True
|
||||
|
||||
# warnings (advisory for now — flip to errors once clean history is established)
|
||||
if os.path.exists("AGENTS.md"):
|
||||
size = os.path.getsize("AGENTS.md")
|
||||
if size > 32768:
|
||||
print(f"::warning file=AGENTS.md::{size} bytes exceeds the 32 KiB context budget")
|
||||
if os.path.exists("CLAUDE.md"):
|
||||
lines = sum(1 for _ in open("CLAUDE.md", encoding="utf-8"))
|
||||
if lines > 40:
|
||||
print(f"::warning file=CLAUDE.md::{lines} lines — expected a thin wrapper over AGENTS.md")
|
||||
for f in ("architecture.md", "conventions.md", "lessons.md"):
|
||||
p = f".agents/memory/{f}"
|
||||
if os.path.exists(p) and os.path.getsize(p) > 25 * 1024:
|
||||
print(f"::warning file={p}::exceeds 25 KB — curate or split")
|
||||
|
||||
sys.exit(1 if hard_fail else 0)
|
||||
EOF
|
||||
@@ -13,7 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Upload dist hash artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist-hashes-current
|
||||
path: artifacts/dist-hashes-current.json
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
# deploy-cache-state CDN purge below. Real build/lint failures still gate the
|
||||
# deploy via the build job.
|
||||
visual-regression:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: steps.visual.outcome == 'failure'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-visual-diffs
|
||||
path: |
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download dist hash artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: dist-hashes-current
|
||||
path: artifacts
|
||||
@@ -401,7 +401,7 @@ jobs:
|
||||
run: npm run test:dm:unit
|
||||
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 35
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
run: npm run test:dm:integration
|
||||
|
||||
e2e-headless:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Daily Design System Audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * *' # Run daily at 09:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# `path: dist` stores the site files at the artifact root (index.html at top),
|
||||
# so the downloaded .zip extracts straight to the nsite site directory.
|
||||
- name: Upload site archive
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bitvid-site-${{ github.ref_name }}-${{ steps.vars.outputs.sha }}
|
||||
path: dist
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Regenerate the visual-regression baselines IN the environment that compares
|
||||
# them.
|
||||
#
|
||||
# The baselines in tests/visual/baselines.json are pixel-compared on
|
||||
# ubuntu-latest CI, but they were historically captured on a developer machine.
|
||||
# Chromium builds and font fallbacks differ between the two ("Inter"/"Fira
|
||||
# Code" are not shipped or installed anywhere, so every environment renders its
|
||||
# own system fallback), so the kitchen-sink diff drifted past MAX_DIFF_RATIO on
|
||||
# CI while passing locally — ci.yml even documents the mismatch as the reason
|
||||
# visual-regression must not block deploys. Capturing the baselines on the same
|
||||
# runner image, browser install and font stack as the comparison removes the
|
||||
# environment variable from the equation entirely.
|
||||
#
|
||||
# Two explicit triggers, both requiring deliberate human intent — regenerating
|
||||
# baselines must never be a side effect of ordinary work:
|
||||
#
|
||||
# 1. workflow_dispatch — only usable once this file reaches the default
|
||||
# branch (GitHub does not index dispatchable workflows on other branches).
|
||||
# 2. A push whose head commit message contains [update-visual-baselines] —
|
||||
# works from unstable today, before any promotion. The workflow's own
|
||||
# commit omits the token, so it cannot retrigger itself.
|
||||
name: Update visual baselines
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [unstable]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-baselines:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.head_commit.message, '[update-visual-baselines]')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js 22
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Mirror the visual-regression job's caches so the regenerated baselines
|
||||
# come from the same dependency tree the comparison job will use.
|
||||
- name: Cache npm directory
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-22-npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-22-npm-
|
||||
|
||||
- name: Cache node_modules
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-22-modules-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-22-modules-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Regenerate baselines
|
||||
run: npm run test:visual:update
|
||||
|
||||
- name: Verify the fresh baselines pass in this environment
|
||||
run: npx playwright test --project=visual
|
||||
|
||||
- name: Commit updated baselines
|
||||
run: |
|
||||
if git diff --quiet -- tests/visual/baselines.json; then
|
||||
echo "Baselines unchanged; nothing to commit."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add tests/visual/baselines.json
|
||||
git commit \
|
||||
-m "test(visual): regenerate baselines on ubuntu-latest" \
|
||||
-m "Captured by the update-visual-baselines workflow so the baseline environment matches the visual-regression job that compares against them."
|
||||
git push
|
||||
+12
@@ -22,3 +22,15 @@ TEST_LOG.mdtest_logs/nip-hits.txt
|
||||
|
||||
# Local site archive for manual nsite upload (npm run archive)
|
||||
bitvid-site.zip
|
||||
.env*
|
||||
|
||||
# Agent runtime state (local only)
|
||||
memory-update.md
|
||||
memory-update.json
|
||||
|
||||
# Agent-local memory state (never committed)
|
||||
.agents/private/
|
||||
.agents/state/
|
||||
fleet/inbox/
|
||||
.task
|
||||
.fleet/
|
||||
@@ -65,12 +65,8 @@ For any nostr related work, please review the nip documentation located in /docs
|
||||
|
||||
### 4a. Cloudflare/S3 upload-path gotchas (audited 2026-06-16)
|
||||
|
||||
* **Two near-identical upload services exist** — `js/services/r2Service.js` (Cloudflare R2) and `js/services/s3UploadService.js` (generic S3). They share `buildR2Key`, `resolveUploadIdentifier`, and the magnet build but are *copies*. **Any fix to one must be mirrored in the other** or they silently drift.
|
||||
* **Storage-key collisions = data loss — FIXED 2026-06-16.** `buildR2Key` (`js/r2.js`) builds `u/<npub>/<namespace>/<slug>.<ext>` where `namespace` used to fall back to the literal `"uploads"` and `slug` to `"video"` (e.g. a non-ASCII filename, or no info-hash) — so a URL-first upload with no info-hash and a duplicate filename overwrote the previous object while the old note still pointed at that URL. WebTorrent uploads were always safe (info-hash namespaces the key = content-addressed). Fix: `computeStorageContentHash(file)` (`js/r2.js`) derives a content-based namespace when no info-hash is available (full SHA-256 for ≤512 MB, sampled metadata+edge fingerprint above that to avoid buffering huge files), wired into **both** `r2Service` and `s3UploadService`. Test: `tests/storage-key-collision.test.mjs`. **If you add a third upload path, reuse `computeStorageContentHash`.**
|
||||
* **Whole-file hash before upload.** When the modal doesn't pre-seed an info-hash, `resolveUploadIdentifier` runs `calculateTorrentInfoHash(file)`, which reads the *entire* file before the upload begins and before any progress is emitted — on multi-GB files this looks like a hang. Emit a "preparing/hashing" status or parallelize.
|
||||
* **Publish can report success unconfirmed.** `publishEventToRelay` (`js/nostrPublish.js`) has an optimistic-success fallthrough when the relay handle exposes no `on()`/`then()` — a note may show "Published" with no relay ACK. Don't tighten blindly (legacy `seen`-only relays rely on it), but log it.
|
||||
* **Silent partial failures.** Thumbnail and `.torrent` upload failures are caught and `warn`-only; the video note still publishes with no user signal. Empty (0-byte) files hit `CompleteMultipartUpload` with `Parts:[]` and surface a cryptic error — guard `file.size === 0`.
|
||||
* **Object cleanup (deletes/edits).** `R2Service.deleteVideoStorage()` removes the backing objects so "deleted" videos aren't left publicly downloadable. Contract to preserve: it's **best-effort and never throws** (must not block note deletion), it derives keys via `collectVideoStorageKeys()` and **only ever touches URLs under the owner's bucket base** (never external/3rd-party URLs), and it skips cleanly when storage is locked/credential-less. Wired into delete (`app.handleDeleteModalConfirm`) and edit-with-replaced-URL (`editModalController.handleSubmit`). Edit cleanup intentionally does **not** delete thumbnails (the new note may still reference them) and only fires when both old and new URLs are present and different.
|
||||
**Invariants:** `js/services/r2Service.js` and `js/services/s3UploadService.js` are near-identical copies — any fix to one must be mirrored in the other; storage keys must stay content-addressed (reuse `computeStorageContentHash` in `js/r2.js` for any new upload path); `R2Service.deleteVideoStorage()` stays best-effort, never throws, and only touches URLs under the owner's bucket base.
|
||||
Full writeup: [`docs/lessons/cloudflare-s3-upload-gotchas.md`](docs/lessons/cloudflare-s3-upload-gotchas.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -324,127 +320,8 @@ Three automated lint checks protect the codebase from common growth problems. Th
|
||||
|
||||
## 14. Playwright Agent Testing Infrastructure
|
||||
|
||||
bitvid includes a test harness and fixtures that let Playwright CLI agents programmatically log in, seed relay data, and inspect app state — no browser extensions or real relays required.
|
||||
|
||||
### Activating Test Mode
|
||||
|
||||
Add `?__test__=1` to the URL, or set `localStorage.__bitvidTestMode__ = "1"` via `addInitScript`. This installs `window.__bitvidTest__` on the page.
|
||||
|
||||
### Overriding Relays
|
||||
|
||||
Point the app at a local mock relay instead of production:
|
||||
|
||||
```
|
||||
?__test__=1&__testRelays__=ws://127.0.0.1:8877
|
||||
```
|
||||
|
||||
Or set `localStorage.__bitvidTestRelays__` to a JSON array of relay URLs.
|
||||
|
||||
### Test Harness API (`window.__bitvidTest__`)
|
||||
|
||||
| Method | Returns | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `loginWithNsec(hexKey)` | `Promise<string>` (pubkey) | Programmatic login, bypasses the modal |
|
||||
| `logout()` | `void` | Clear active signer |
|
||||
| `getAppState()` | `{ isLoggedIn, activePubkey, relays, ... }` | Inspect current state |
|
||||
| `getFeedItems()` | `Array<{ title, pubkey, dTag, hasUrl, hasMagnet }>` | Scrape video cards from DOM |
|
||||
| `waitForFeedItems(n, ms)` | `Promise<Array>` | Wait for N cards to appear |
|
||||
| `waitForSelector(sel, ms)` | `Promise<true>` | Wait for a DOM element |
|
||||
| `getRelayHealth()` | `{ relays, unreachable, backoff }` | Relay connection status |
|
||||
| `applyRelayOverrides(urls)` | `boolean` | Redirect relay connections |
|
||||
| `setTestRelays(urls)` | `{ ok, relays }` | Align relayManager + nostrClient to a deterministic relay set |
|
||||
| `setSignerDecryptBehavior(mode, opts?)` | `{ ok, mode, ... }` | Force decrypt behavior (`passthrough`, `timeout`, `error`, `delay`) for signer-based list sync tests |
|
||||
| `getListSyncEvents()` | `Array<{ source, at, detail }>` | Read captured sync timeline (`bitvid:auth-loading-state` + `userBlocks` status) |
|
||||
| `clearListSyncEvents()` | `void` | Reset captured sync events between assertions |
|
||||
| `waitForListSyncEvent(criteria, ms?)` | `Promise<Event>` | Wait for sync milestones (`status`, `reason`, `source`) |
|
||||
| `nostrClient` | `NostrClient` | Direct access for advanced use |
|
||||
|
||||
### Mock Relay (`scripts/agent/simple-relay.mjs`)
|
||||
|
||||
Start with `startRelay(port, { httpPort })`. Alongside the Nostr WebSocket protocol, exposes an HTTP API:
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/seed` | `POST` | Inject signed events (single object or array) |
|
||||
| `/events` | `GET` | List all stored events |
|
||||
| `/events` | `DELETE` | Clear all events |
|
||||
| `/health` | `GET` | Event count + connection count |
|
||||
|
||||
Programmatic API: `relay.seedEvent(event)`, `relay.clearEvents()`, `relay.getEvents()`.
|
||||
|
||||
### Playwright Fixture (`tests/e2e/helpers/bitvidTestFixture.ts`)
|
||||
|
||||
Import `test` and `expect` from the fixture for tests that need the full stack:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "./helpers/bitvidTestFixture";
|
||||
|
||||
test("agent can seed and view videos", async ({ page, gotoApp, loginAs, seedEvent }) => {
|
||||
await seedEvent({ title: "Test Video", url: "https://example.com/v.mp4" });
|
||||
await gotoApp();
|
||||
await loginAs(page);
|
||||
// ... assertions
|
||||
});
|
||||
```
|
||||
|
||||
**Available fixture values:**
|
||||
|
||||
| Fixture | Type | Purpose |
|
||||
|---------|------|---------|
|
||||
| `relay` | Relay instance | Auto-started/stopped per test |
|
||||
| `seedEvent(video)` | `(TestVideoEvent) => Promise` | Create a signed kind 30078 event and inject it |
|
||||
| `seedRawEvent(event)` | `(any) => Promise` | Inject a pre-built event |
|
||||
| `clearRelay()` | `() => Promise` | Wipe all relay events |
|
||||
| `gotoApp(path?)` | `(string?) => Promise` | Navigate with test mode + relay overrides |
|
||||
| `loginAs(page)` | `(Page) => Promise<string>` | Login with deterministic test key |
|
||||
| `setTestRelays(page, relays)` | `(Page, string[]) => Promise<{ok, relays}>` | Force relay alignment through harness + relayManager |
|
||||
| `setDecryptBehavior(page, mode, opts?)` | `(Page, mode, opts?) => Promise` | Force signer decrypt behavior (`passthrough`, `timeout`, `error`, `delay`) |
|
||||
| `startDiagnostics(page, opts?)` | `(Page, opts?) => Promise<{stop()}>` | Capture console/page errors and list-sync telemetry for assertions |
|
||||
| `testPubkey` | `string` | Hex pubkey of the test key |
|
||||
| `relayUrl` | `string` | WebSocket URL of the mock relay |
|
||||
|
||||
### `data-testid` Selectors
|
||||
|
||||
Use these for stable element targeting:
|
||||
|
||||
| Selector | Element |
|
||||
|----------|---------|
|
||||
| `[data-testid="login-button"]` | Header login button |
|
||||
| `[data-testid="upload-button"]` | Header upload button (hidden until logged in) |
|
||||
| `[data-testid="profile-button"]` | Header profile button (hidden until logged in) |
|
||||
| `[data-testid="profile-permission-prompt"]` | Profile list-sync permission CTA container |
|
||||
| `[data-testid="profile-permission-prompt-button"]` | Profile list-sync permission/retry CTA button |
|
||||
| `[data-testid="search-input"]` | Header search field |
|
||||
| `[data-testid="login-modal"]` | Login modal container |
|
||||
| `[data-testid="login-provider-button"]` | Login provider option buttons |
|
||||
| `[data-testid="nsec-secret-input"]` | Private key textarea in nsec login |
|
||||
| `[data-testid="nsec-submit"]` | Nsec login submit button |
|
||||
| `[data-testid="upload-modal"]` | Upload modal container |
|
||||
| `[data-testid="upload-title"]` | Video title input |
|
||||
| `[data-testid="upload-url"]` | Video URL input |
|
||||
| `[data-testid="upload-magnet"]` | Magnet link input |
|
||||
| `[data-testid="upload-submit"]` | Publish button |
|
||||
| `[data-testid="hashtags-sync-status"]` | Profile hashtag sync status message |
|
||||
| `[data-testid="subscriptions-sync-status"]` | Profile subscriptions sync status message |
|
||||
| `[data-testid="blocked-refresh-button"]` | Profile blocked creators refresh button |
|
||||
| `[data-testid="blocked-sync-status"]` | Profile blocked creators sync status message |
|
||||
| `[data-testid="blocked-list"]` | Blocked creators list container |
|
||||
| `[data-testid="blocked-empty-state"]` | Blocked creators empty state |
|
||||
| `[data-testid="video-modal"]` | Video player modal |
|
||||
| `[data-testid="video-card"]` | Individual video card in the feed |
|
||||
| `[data-testid="video-list"]` | Video feed grid container |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `js/testHarness.js` | Test harness module (installs `window.__bitvidTest__`) |
|
||||
| `scripts/agent/simple-relay.mjs` | Mock relay with HTTP seeding API |
|
||||
| `tests/e2e/helpers/bitvidTestFixture.ts` | Reusable Playwright fixture |
|
||||
| `tests/e2e/agent-testability.spec.ts` | Infrastructure validation tests |
|
||||
| `scripts/playwright/run-extension-persistent.mjs` | Dedicated persistent-context launcher for extension smoke tests |
|
||||
| `docs/testing/playwright-integration-recommendations-2026-02-22.md` | Playwright/extension integration recommendations and follow-ups |
|
||||
| `docs/testing/playwright-function-coverage.md` | Playwright coverage integration, latest baseline metrics, and improvement plan |
|
||||
bitvid ships a test harness (`window.__bitvidTest__`, enabled via `?__test__=1`), a mock relay (`scripts/agent/simple-relay.mjs`), and a reusable Playwright fixture (`tests/e2e/helpers/bitvidTestFixture.ts`) so agents can programmatically log in, seed relay data, and inspect app state — no browser extensions or real relays required.
|
||||
Full reference (harness API, mock relay, fixtures, `data-testid` selectors): [`docs/testing/agent-playwright-harness.md`](docs/testing/agent-playwright-harness.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -553,137 +430,37 @@ When finishing a task or handing off, provide:
|
||||
|
||||
## 16. Scenario-First Tests & Test Integrity (Dark Factory Standard)
|
||||
|
||||
In this repo, **validation replaces code review**. That means our test/scenario system is the *only* thing standing between “working software” and “green-but-worthless software.”
|
||||
|
||||
Agents will naturally optimize for short-term goals (e.g., “get CI green”) unless we constrain them. If we ever allow “edit tests until they pass,” we destroy the signal, and over time the suite becomes meaningless.
|
||||
|
||||
### Core philosophy: scenarios > checklists
|
||||
|
||||
We treat tests as **behavioral specifications** expressed as **scenarios**:
|
||||
- “User stories” / end-to-end behaviors (Given/When/Then or equivalent)
|
||||
- Assertions focus on **externally observable outcomes** at boundaries (API responses, persisted state, emitted events, CLI output, UI state), not internal call sequences.
|
||||
- We prefer *minimal coupling* to implementation details so refactors don’t require rewriting “truth.”
|
||||
|
||||
(StrongDM reached the same conclusion: repo-local tests are easy to reward-hack; scenarios and satisfaction-based validation reduce “teaching to the test.”)
|
||||
|
||||
### Non-negotiable constitution (anti-cheat)
|
||||
|
||||
**Hard rules (always on):**
|
||||
1) **Never weaken, delete, or rewrite a test just to make CI pass.**
|
||||
2) **Never change expected behavior to match buggy output** (“rubber-stamp the snapshot”, “update golden”, “adjust assertion”) unless it is a **spec correction** (see below).
|
||||
3) Do not “fix” flaky tests with retries, sleeps, timeouts, or looser assertions. **Remove nondeterminism** instead (control time, randomness, IO, network).
|
||||
4) Prefer black-box assertions at system boundaries; avoid tests that merely mirror internal logic.
|
||||
|
||||
### Definitions
|
||||
|
||||
- **Scenario**: a behavioral spec (often end-to-end) describing what must happen for a user/system in a real environment.
|
||||
- **Invariant**: a property that must always hold across many inputs/states (great for property-based or metamorphic testing).
|
||||
- **Holdout scenario**: an evaluation scenario kept outside the agent-editable area to reduce overfitting / reward-hacking.
|
||||
|
||||
### When tests may change (spec correction protocol)
|
||||
|
||||
Changing a test expectation is allowed **only** when the test was enforcing the wrong behavior.
|
||||
|
||||
If you believe a test should change:
|
||||
1) Identify the **scenario/spec** that defines correct behavior.
|
||||
2) Explain precisely why the old expectation was wrong.
|
||||
3) Replace it with an **equally strict or stricter** behavior-based check.
|
||||
4) Record a **Test Integrity Note** (below) and, if applicable, a short spec note (e.g., `docs/spec_changes/<date>-<slug>.md`).
|
||||
|
||||
If you cannot point to scenario/spec truth, **do not change the test**. Add a “Needs Spec Clarification” note instead.
|
||||
|
||||
### “Test Integrity Note” (required for any PR touching tests)
|
||||
|
||||
Every PR that adds/changes tests must include this machine-readable block in the PR description (or in a `TEST_INTEGRITY.md` entry referenced by the PR):
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests" | "refactor_tests" | "spec_correction" | "flake_fix"]
|
||||
scenarios:
|
||||
- id: SCN-<slug>
|
||||
given: "<preconditions>"
|
||||
when: "<stimulus>"
|
||||
then: "<observable outcomes>"
|
||||
observable_outcomes:
|
||||
- "<what the user/system can observe at the boundary>"
|
||||
determinism_controls:
|
||||
- "<fake clock / fixed seed / hermetic env / service virtualization>"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "hard-coded return value"
|
||||
- "over-mocking internal logic"
|
||||
- "snapshot rubber-stamping"
|
||||
- "retry/sleep-based flake masking"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
### Test portfolio guidance (scenario-first, still layered)
|
||||
|
||||
We want a balanced suite:
|
||||
|
||||
- **Scenario/E2E**: user-story validation at boundaries (few, high value, high fidelity)
|
||||
- **Integration/contract**: service boundaries, serialization, persistence, workflows
|
||||
- **Unit/invariants**: properties that kill trivial cheats (“return true” shouldn’t survive)
|
||||
|
||||
For cheat resistance, strongly prefer:
|
||||
- invariants/property-based tests for critical logic
|
||||
- metamorphic relations where “exact expected output” is hard
|
||||
- periodic mutation testing to “test the tests” (surviving mutants indicate missing assertions)
|
||||
|
||||
### External dependency realism (service virtualization / “digital twins”)
|
||||
|
||||
Where third-party services are involved, we prefer deterministic “behavioral clones” (mocks/stubs at the API boundary that reproduce edge cases and contracts) over live calls. This keeps scenarios realistic without being flaky or rate-limited, and it makes high-volume scenario validation affordable.
|
||||
|
||||
### Role: Test Integrity & Scenario Spec Agent (Enforcer)
|
||||
|
||||
This repo includes (or assumes) a dedicated agent role whose only job is to protect validation integrity.
|
||||
|
||||
This agent’s goal is NOT “make CI green.” Its goal is “make the suite reflect reality and reject fake passes.”
|
||||
|
||||
**Authority:**
|
||||
- May add new scenarios, invariants, and tests.
|
||||
- May refactor tests to be more behavioral and less procedural.
|
||||
- May improve determinism (fixed seeds, fake clocks, hermetic env).
|
||||
- May only change expectations via the Spec Correction Protocol above.
|
||||
|
||||
**Prohibitions:**
|
||||
- Must never weaken tests to pass.
|
||||
- Must never edit holdout scenarios (if configured).
|
||||
- Must never solve flakiness via retries/sleeps/loosening.
|
||||
|
||||
**Deliverables each run:**
|
||||
- Scenario list (Given/When/Then)
|
||||
- Proposed test diffs
|
||||
- Test Integrity Note
|
||||
- “Cheat vectors blocked” summary (what trivial implementations it prevents)
|
||||
**Invariant:** validation replaces code review in this repo — never weaken, delete, or rewrite a test merely to make it pass, and never mask flakiness with retries/sleeps/looser assertions. Expectation changes are allowed only via the Spec Correction Protocol with an accompanying Test Integrity Note.
|
||||
Read the full standard before adding or modifying any test: `docs/testing/test-integrity.md`.
|
||||
|
||||
---
|
||||
|
||||
## 17. NIP-07 Signer Reliability & Encrypted-List Loading (hard-won, 2026-06-16)
|
||||
|
||||
The single biggest source of "DMs / hashtags / watch-history / block & subscription lists won't load after login" is **not** bitvid — it's an **unresponsive NIP-07 signer**. The extension's MV3 background service-worker can die or its content-script↔worker channel can orphan, after which raw `window.nostr` calls hang forever. No client change can force a dead signer to answer.
|
||||
**Invariant:** the #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an unresponsive NIP-07 signer, not bitvid — diagnose with a raw `window.nostr` probe first, and do not regress the five resilience invariants (capped relay fan-out, NIP-07 circuit breaker, re-thrown transient decrypt errors, generous decrypt budgets, lazy modal population).
|
||||
Full writeup: `docs/lessons/nip07-signer-reliability.md`. See also `docs/KNOWN_BUGS.md` #0.
|
||||
|
||||
* **Diagnose first, don't guess.** Run a single raw `window.nostr` probe in the page console (`getPublicKey → nip04.encrypt → nip04.decrypt`) that bypasses bitvid entirely. If *that* hangs, it's the extension/environment. Recommend a well-maintained signer (nos2x, Alby); KeysBand's dead worker was the root cause and switching to nos2x fixed everything.
|
||||
* **Resilience invariants — do not regress these** (see `docs/KNOWN_BUGS.md` #0 for the full history and the files):
|
||||
1. **Cap relay fan-out** (`js/nostr/toolkit.js` `capReadRelays`, ≤8, user-relays-first + 2 reserved default slots). An uncapped cold-login REQ storm to ~20 dead NIP-65 relays starves the single-threaded signer's postMessage round-trips.
|
||||
2. **Circuit breaker** on the NIP-07 channel (`js/nostr/nip07Permissions.js`): after N consecutive *timeouts* fast-fail instead of hanging ~15s each. Channel-death errors must count toward opening (not reset it); interactive permission prompts bypass it; one periodic probe detects recovery.
|
||||
3. **Never swallow a transient decrypt error as an empty result.** Re-throw channel-death/timeout sub-errors so the retry path runs — returning `[]` turns "signer is slow" into "user has no blocks/lists" and kills retries.
|
||||
4. **Generous decrypt budget** (~25–30s/call, ~60s backoff cap); a 6s timeout kills slow-but-responsive signers mid-decrypt. Handshake variant timeout is ~20s for the same reason.
|
||||
5. **One actionable user notice** (`js/utils/signerHealthNotice.js`) after a few timeouts with `signerStatus: "present"` — not a silent forever-retry.
|
||||
* **Don't render into closed modals at login.** Once a responsive signer makes decryption instant, eagerly populating every profile panel at login (friends avatars, subscriptions, blocks, DM summaries) freezes the main thread ~10–15s. Populate each pane lazily on open (`selectPane`) and gate data-change re-render listeners on "is the view open".
|
||||
* **Deterministic testing.** Reproduce signer-dependent bugs headlessly with a fake `window.nostr` (Playwright `exposeBinding` + `addInitScript`) + a mock relay + configurable channel models (healthy/slow/overload/dead, latency override). See `scripts/perf/nip07-channel-sim.mjs`.
|
||||
---
|
||||
|
||||
## Repo memory
|
||||
|
||||
Curated agent memory lives in `.agents/` (index `.agents/MEMORY.md`); read it first. Propose additions in `.agents/proposals/`; `.agents/memory/` changes only via reviewed commits. Code/tests/config outrank memory.
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
Please read these documents next.
|
||||
Read `docs/nostr-event-schemas.md` and `js/nostrEventSchemas.js` next; keep new nostr kinds centralized there and the doc up to date.
|
||||
|
||||
* `docs/nostr-event-schemas.md`
|
||||
* `js/nostrEventSchemas.js`
|
||||
# Agent fleet
|
||||
|
||||
And if you need to create new nostr kinds please keep the logic centralized there in `nostrEventSchemas.js` and the `nostr-event-schemas.md` up to date.
|
||||
This repo uses agent-fleet-kit. The `agent-fleet` MCP server (`fleet-mcp`) is a convenience. Editing backlog JSON and mailbox files directly is fine. Do not hand-edit the landing queue while a drain is merging, and do not push main.
|
||||
|
||||
**End of AGENTS.md**
|
||||
- `area_info` for the owner of a path.
|
||||
- `backlog_claim` with an item id marks it in-progress. Pick the item from PROGRAM.md. Without an id the tool does not choose one. Idle is a correct result.
|
||||
- `mailbox_read` at the start of a turn; `mailbox_send` to reach a peer. Mail is always delivered. The prompt says when to act.
|
||||
- `commit` to stage and commit in your area worktree. Codex `workspace-write` keeps `.git` read-only, so do not run `git commit` yourself. Pass `message` and the file paths (or `all: true`), and `cwd` or `area` when this server is not already in the worktree. `submit: true` also queues the tip. A doesNotOwn path warns and still commits. The same operation is `fleet commit -m "..." --area <id> [--submit] -- <paths>`.
|
||||
- `landq_submit` queues the committed tip and starts a drain. A dirty worktree warns; only HEAD is queued. Then go back to work. At your next natural checkpoint, `land_status` (or `fleet landq status <id>`) is a quick lookup, and its wait is optional. Never infer the lander or the land result from silence. States: queued, gating, landed with the main sha, rejected with the reason, or unknown. If no drain is alive, the answer says so and names `fleet lander start`.
|
||||
- A scarce machine is a prompt convention: say so in `.task` and wait if a peer has it. There is no slot lock.
|
||||
|
||||
Sessions, worktrees, and cron are the `fleet` CLI. Submit starts the drain; do not run a second merge by hand.
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This document provides context and guidance for AI assistants working with the bitvid codebase. It explains the project structure, development workflows, conventions, and key architectural decisions.
|
||||
|
||||
**Canonical guidance:** `AGENTS.md` is the canonical agent guidance for this repo; `CLAUDE.md` holds Claude-oriented codebase orientation that layers on top of it.
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
@@ -312,7 +314,7 @@ const dTag = event.tags.find(t => t[0] === 'd')?.[1];
|
||||
|
||||
### NIP-07 Signer Reliability (hard-won)
|
||||
|
||||
The #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an **unresponsive NIP-07 signer**, not bitvid. The extension's MV3 background worker can die and `window.nostr` calls then hang forever — no client change fixes a dead signer. **Diagnose with a raw `window.nostr` probe** (`getPublicKey → nip04.encrypt → nip04.decrypt`) in the console; if that hangs, it's the extension (recommend nos2x/Alby). See `AGENTS.md` §17 and `docs/KNOWN_BUGS.md` #0.
|
||||
The #1 cause of "DMs / hashtags / watch-history / lists won't load after login" is an **unresponsive NIP-07 signer**, not bitvid. The extension's MV3 background worker can die and `window.nostr` calls then hang forever — no client change fixes a dead signer. **Diagnose with a raw `window.nostr` probe** (`getPublicKey → nip04.encrypt → nip04.decrypt`) in the console; if that hangs, it's the extension (recommend nos2x/Alby). See `docs/lessons/nip07-signer-reliability.md` (summarized in `AGENTS.md` §17) and `docs/KNOWN_BUGS.md` #0.
|
||||
|
||||
Resilience invariants — **do not regress**:
|
||||
|
||||
@@ -326,78 +328,7 @@ Resilience invariants — **do not regress**:
|
||||
|
||||
## Testing & Validation: Scenario-First, Cheat-Resistant (Dark Factory Rules)
|
||||
|
||||
You are operating inside a “Dark Factory” workflow where **validation replaces human code review**. Passing tests is only meaningful if tests are truthful.
|
||||
|
||||
### Prime directive
|
||||
|
||||
**Never alter a test merely to make it pass.**
|
||||
If you change tests to match buggy behavior, you are destroying the only feedback signal and producing code with no real-world value.
|
||||
|
||||
StrongDM describes the exact failure mode we are preventing: agents discovering shortcuts (“return true”) and narrow tests being reward-hacked, leading to a shift from “tests” to scenario-based validation.
|
||||
|
||||
### How to think about tests here
|
||||
|
||||
Tests are not procedural checklists. They are **behavioral specifications**:
|
||||
- Write tests as **scenarios** (Given/When/Then or equivalent).
|
||||
- Assert **externally observable outcomes** at boundaries.
|
||||
- Avoid “testing the implementation” (internal call sequences, private methods, mirrored logic).
|
||||
- Prefer deterministic/harmonic execution: fixed seeds, controlled clocks, hermetic IO, virtualized dependencies.
|
||||
|
||||
### Prohibited behaviors (reward-hacking patterns)
|
||||
|
||||
Do NOT:
|
||||
- weaken assertions, broaden tolerances, or reduce coverage to get green
|
||||
- update snapshots/goldens without proving the change is intended behavior
|
||||
- add retries/sleeps/timeouts to hide flakes
|
||||
- mock so heavily that tests can pass with a broken system
|
||||
- “hard-code to the test” (single fixtures, single input paths, brittle constants)
|
||||
|
||||
### Allowed test changes: Spec Correction Protocol
|
||||
|
||||
You may change test expectations only if the test is wrong relative to intended behavior.
|
||||
|
||||
Required steps:
|
||||
1) Identify the scenario/spec that defines correct behavior.
|
||||
2) Explain why the previous expectation was incorrect.
|
||||
3) Replace it with an equally strict (or stricter) behavioral assertion.
|
||||
4) Provide a **Test Integrity Note** (machine-readable) documenting the change.
|
||||
|
||||
If you cannot justify a change with scenario/spec truth: **do not change the test**. Instead, output a “Needs Spec Clarification” note and propose candidate scenarios.
|
||||
|
||||
### Required output when you touch tests
|
||||
|
||||
Include this YAML block in your PR description (or reference it from a `TEST_INTEGRITY.md` entry):
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests" | "refactor_tests" | "spec_correction" | "flake_fix"]
|
||||
scenarios:
|
||||
- id: SCN-<slug>
|
||||
given: "<preconditions>"
|
||||
when: "<stimulus>"
|
||||
then: "<observable outcomes>"
|
||||
observable_outcomes:
|
||||
- "<boundary/output asserted>"
|
||||
determinism_controls:
|
||||
- "<fixed seed / fake clock / hermetic env / service virtualization>"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "hard-coded return value"
|
||||
- "over-mocking internal logic"
|
||||
- "snapshot rubber-stamping"
|
||||
- "retry/sleep-based flake masking"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
### If external services are involved
|
||||
|
||||
Prefer boundary-level service virtualization (“digital twins” / behavioral clones) so scenario tests remain:
|
||||
- high fidelity (realistic edge cases)
|
||||
- deterministic and replayable
|
||||
- not rate-limited / not flaky
|
||||
(StrongDM’s “Digital Twin Universe” is the reference pattern.)
|
||||
The testing and validation invariant is stated in `AGENTS.md` §16 "Scenario-First Tests & Test Integrity (Dark Factory Standard)", and the full standard lives in [`docs/testing/test-integrity.md`](docs/testing/test-integrity.md). In short: validation replaces human code review here, tests are behavioral scenarios that assert externally observable outcomes at boundaries, and you must **never alter a test merely to make it pass** — expectation changes are allowed only via the Spec Correction Protocol with an accompanying Test Integrity Note. Read `docs/testing/test-integrity.md` in full before adding or modifying any test.
|
||||
|
||||
---
|
||||
|
||||
@@ -438,7 +369,7 @@ View with: `./scripts/show-artifacts.sh`
|
||||
|
||||
### Agent E2E Testing (Playwright)
|
||||
|
||||
bitvid includes a test harness for programmatic Playwright testing without browser extensions or real relays. See `AGENTS.md` Section 14 for the full reference. Quick start:
|
||||
bitvid includes a test harness for programmatic Playwright testing without browser extensions or real relays. See `docs/testing/agent-playwright-harness.md` for the full reference (summarized in `AGENTS.md` §14). Quick start:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from "./helpers/bitvidTestFixture";
|
||||
@@ -457,7 +388,7 @@ Key capabilities:
|
||||
- **Mock relay**: Each test gets an isolated in-memory relay with HTTP seeding
|
||||
- **Relay override**: `?__test__=1&__testRelays__=ws://localhost:8877` redirects all connections
|
||||
- **State inspection**: `window.__bitvidTest__.getAppState()` returns login status, relays, etc.
|
||||
- **Stable selectors**: All key elements have `data-testid` attributes (see AGENTS.md Section 14)
|
||||
- **Stable selectors**: All key elements have `data-testid` attributes (see `docs/testing/agent-playwright-harness.md`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -242,3 +242,12 @@ If you find a vulnerability, please email **[[email protected]](mailto:sec
|
||||
## Trademark
|
||||
|
||||
The **bitvid** name and logos are trademarks (see `TRADEMARKS.md`). Don’t use them in ways that imply official sponsorship or confuse users. “Powered by bitvid” attribution is encouraged.
|
||||
|
||||
## AI-assisted contributions
|
||||
|
||||
AI-assisted changes are subject to the same testing, security, and review
|
||||
requirements as any other change; the contributor remains responsible for
|
||||
understanding what they submit. Do not submit unvalidated generated code,
|
||||
credentials, or agent-generated trusted memory without evidence and review.
|
||||
The canonical backlog and planning rules are in `AGENTS.md`; durable agent
|
||||
plans live under `.agents/plans/`.
|
||||
+26
-14
@@ -3,11 +3,10 @@
|
||||
## Test Infrastructure
|
||||
|
||||
### Missing `jsdom` Dependency
|
||||
- **Status:** Active
|
||||
- **Status:** Resolved (2026-07-30)
|
||||
- **Detected:** 2026-02-26
|
||||
- **Description:** Multiple unit tests fail with `ERR_MODULE_NOT_FOUND` because `jsdom` is imported but not installed in the project.
|
||||
- **Impact:** Prevents running unit tests that rely on DOM simulation.
|
||||
- **Remediation:** `npm install --save-dev jsdom`
|
||||
- **Description:** Unit tests failed with `ERR_MODULE_NOT_FOUND` because `jsdom` was imported but not installed.
|
||||
- **Resolution:** `jsdom` has been a devDependency for some time (28.1.0 installed and in `package.json`); the jsdom-based suites run clean.
|
||||
|
||||
## Run Notes (2026-03-04)
|
||||
|
||||
@@ -22,19 +21,32 @@
|
||||
- **Description:** `tests/nostr-send-direct-message.test.mjs` must pass `Uint8Array` secret keys to `getPublicKey/finalizeEvent` for current `nostr-tools`.
|
||||
- **Impact:** Passing hex strings causes failures like `expected Uint8Array, got type=string`.
|
||||
|
||||
## Run Notes (2026-06-20)
|
||||
## Run Notes (2026-06-20, updated 2026-07-30)
|
||||
|
||||
### Additional E2E specs flake under full parallel load (pass in isolation)
|
||||
### `login-flows.spec.ts:149` — upload/profile buttons stay visible after logout
|
||||
- **Status:** Active — NOT merely a parallel-load flake
|
||||
- **Description:** Previously filed as a worker-contention flake that "passes deterministically in isolation." On 2026-07-28 it failed **in isolation** as well (chromium, local and CI `e2e-headless`), while passing in the CI `e2e-tests (e2e)` job — environment-sensitive, not load-sensitive. Direct probes of the same flow (raw `__bitvidTest__.loginWithNsec` → `logout()` against a mock relay) hide the button correctly, so the failure is specific to something in the Playwright fixture path. An attempted fix (running `applyLoggedOutUiState()` before the awaited NWC/profile teardown in `_executeAuthLogout`) did not change the outcome and was reverted rather than shipped unverified.
|
||||
- **Impact:** One e2e failure in `e2e-headless`; logout hides gated UI correctly in manual testing and direct harness probes.
|
||||
- **Next step:** Investigate alongside pre-launch TODO #33 ("logout logs out everyone" / NIP-46 session persistence) — same subsystem.
|
||||
|
||||
### `video-upload-publish-lifecycle.spec.ts:49` / `:79` flake under full parallel load
|
||||
- **Status:** Active
|
||||
- **Description:** Under the full `test:e2e` run (378 tests, 4 workers) these intermittently fail on `not.toBeVisible`/feed-appearance timeouts, but pass deterministically when re-run in isolation (`--workers=1`): `login-flows.spec.ts:149` (upload/profile buttons hide after logout), `video-upload-publish-lifecycle.spec.ts:49` and `:79` (URL-first publish appears / owner delete). Same root cause as the Firefox feed-hydration flakes above — worker resource contention, not an app regression (verified: pass in isolation on the current branch).
|
||||
- **Impact:** Full-suite e2e is flaky; isolated reruns are green.
|
||||
- **Description:** Under the full `test:e2e` run these intermittently fail on feed-appearance timeouts but pass on retry (CI marks them flaky). Worker resource contention.
|
||||
- **Impact:** Full-suite e2e occasionally needs retries; isolated reruns are green.
|
||||
|
||||
### `webseed-playback.spec.ts:31` fails on Firefox headless (CDN mode)
|
||||
- **Status:** Active (pre-existing — reproduced on pre-refactor commit `1b11cb1b`)
|
||||
- **Description:** "plays directly from HTML link (CDN mode)" fails on `--project=e2e-firefox` with `#modalVideo` `src` empty instead of the test's archive.org URL. The Chromium variant and the WebTorrent-fallback variant (`:63`) both pass. Firefox-headless-specific (external URL reachability/codec), not an app regression.
|
||||
- **Impact:** One Firefox-only e2e failure; CDN playback works on Chromium and in manual testing.
|
||||
- **Status:** Resolved (2026-07-28, `f48150ed`)
|
||||
- **Description:** The test asserted the **final** `src` equals the CDN URL while simultaneously tolerating "No playable source found" in its status assertion — contradictory, because a failed hosted attempt calls `resetVideoElement()`, which clears `src`. Headless CI cannot decode the archive.org H.264 sample, so the cleared-src path was routine there.
|
||||
- **Resolution:** The test now records every `src` the player attaches via a MutationObserver and asserts the CDN URL was attached — the actual behavior under test. Green in both `e2e` and `e2e-firefox` CI jobs.
|
||||
|
||||
### `uploadModal-reset.test.mjs` ("UploadModal Reset Logic") hangs/cancels
|
||||
- **Status:** Active (pre-existing — reproduced on pre-refactor commit `1b11cb1b`)
|
||||
- **Description:** The jsdom-based UploadModal reset suite intermittently fails with `cancelledByParent` / "Promise resolution is still pending but the event loop has already resolved" (~12s hang). It passes or fails depending on full-suite test ordering/timing (an async-hang flake, likely a torrent-metadata/upload promise not settling under the mocked services). Not a regression from the MediaUploader extraction — it fails identically on `1b11cb1b`.
|
||||
- **Impact:** One unit suite can report `not ok 1`; the runner still exits 0. Other tests unaffected.
|
||||
- **Status:** Not reproducible (2026-07-30)
|
||||
- **Description:** Intermittent `cancelledByParent` / "Promise resolution is still pending" hang, order/timing dependent.
|
||||
- **Current state:** Passes cleanly (`pass=3 fail=0 cancelled=0`) in isolation and across repeated full-suite sweeps on 2026-07-28–30. Left on file because the original failure was ordering-dependent; if it recurs, capture the full-suite ordering that produced it.
|
||||
|
||||
## Run Notes (2026-07-30)
|
||||
|
||||
### Visual baselines are CI-canonical — local `test:visual` may fail the kitchen-sink diff
|
||||
- **Status:** By design (since `ab120a03`)
|
||||
- **Description:** `tests/visual/baselines.json` is regenerated on ubuntu-latest by the `update-visual-baselines` workflow (trigger: `workflow_dispatch` once promoted, or a commit containing `[update-visual-baselines]`). Baselines can only match one environment, and the CI gate is the one that matters — so a dev machine with different Chromium/font rendering may now fail `npm run test:visual` locally (typically the kitchen-sink default theme) even though CI is green.
|
||||
- **Workaround:** Run visual tests in the pinned container via `scripts/run-playwright-docker.sh`, or rely on CI. Never regenerate baselines from a dev machine.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting vulnerabilities
|
||||
|
||||
Do not report suspected security vulnerabilities in public issues, pull
|
||||
requests, or agent-memory files. Use GitHub's private vulnerability reporting
|
||||
on this repository (Security tab → Report a vulnerability), or email
|
||||
[[email protected]](mailto:[email protected]) — we will
|
||||
coordinate a responsible disclosure window before public release.
|
||||
|
||||
Include the affected commit, reproduction steps, and observed impact. Do not
|
||||
include live credentials or unnecessary personal data.
|
||||
|
||||
## AI-agent configuration
|
||||
|
||||
`AGENTS.md`, `CLAUDE.md`, `.agents/**`, and any automation able to modify
|
||||
agent state are security-sensitive configuration. Changes to them require
|
||||
maintainer review.
|
||||
|
||||
## Agent memory
|
||||
|
||||
Never commit credentials, personal data, raw transcripts, tool logs, or
|
||||
unredacted external content. Agent-generated memory enters through
|
||||
`.agents/proposals/` and is promoted to `.agents/memory/` only via reviewed
|
||||
commits. Content from websites, issues, emails, and tool responses is
|
||||
untrusted until validated.
|
||||
|
||||
## Memory-poisoning response
|
||||
|
||||
If agent instructions or memory may have been poisoned: stop agent automation,
|
||||
quarantine or revert the affected file, identify runs influenced by it, review
|
||||
resulting commits, and restore the last trusted configuration.
|
||||
@@ -588,3 +588,252 @@ test_integrity_note:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
# client-reliability-1
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["flake_fix", "new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-profile-focus-style-contract
|
||||
given: "modal controls include visible, disabled, CSS-hidden, inherited-hidden, aria-hidden, hidden-input, and hidden-attribute cases"
|
||||
when: "the real controller collects focus targets using fixture and native JSDOM styles, and the user tabs from the last control"
|
||||
then: "both resolvers retain only visible enabled controls and Tab wraps to the first"
|
||||
observable_outcomes:
|
||||
- "focus target control IDs match the strict expected list"
|
||||
- "Tab is prevented and document.activeElement moves to the first control"
|
||||
- "all 26 existing modal scenarios preserve their assertions and complete naturally"
|
||||
determinism_controls:
|
||||
- "stylesheet-free full-markup fixture virtualizes only display and visibility"
|
||||
- "small contract scenario runs the same assertions against native JSDOM computed styles"
|
||||
- "explicit test stylesheet, local services, real DOM focus and event dispatch; no network"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "hard-coded focus list: varied hidden and visible controls"
|
||||
- "over-mocking controller behavior: real updateFocusTrap and keyboard listener"
|
||||
- "style resolver rubber-stamping: native resolver must satisfy the same scenario"
|
||||
- "retry/sleep-based flake masking: no timeouts, retries, or sleeps added"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
Mutation evidence: removing the product display/visibility exclusion causes this scenario to fail with the hidden control IDs in its focus list (exit 1, 1 failed/0 passed). Mutant reverted. This fixes expensive JSDOM style evaluation in the test harness, not a demonstrated browser defect. Application CSS/layout remains outside this unit fixture's scope.
|
||||
|
||||
Fixture mutation: omitting native hidden-attribute filtering also fails the contract (exit 1, 1 failed/0 passed); restored before final checks.
|
||||
|
||||
---
|
||||
|
||||
# client-reliability-2
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["flake_fix", "new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-concurrent-init-hermetic-subscription
|
||||
given: "two init callers share a pool initialization held at a deterministic barrier, with a virtual successful relay connection"
|
||||
when: "pool initialization is released and the second caller opens then closes a video subscription"
|
||||
then: "the second caller waits for pool readiness, real L1 feed setup reaches the fake pool, and unsubscribe closes the transport exactly once with no reconnect timer"
|
||||
observable_outcomes:
|
||||
- "second init promise remains pending until the pool barrier is released"
|
||||
- "pool is ready after awaiting the second caller; both init promises settle"
|
||||
- "post-init subscription requests video events from a nonempty relay set"
|
||||
- "subscription transport closes once, including repeated unsubscribe"
|
||||
- "the process exits naturally with no public relay IO or reconnect retries"
|
||||
determinism_controls:
|
||||
- "deferred pool barrier replaces the old 50ms delay"
|
||||
- "fake pool at the actual transport boundary; real init, L1, and feed cleanup"
|
||||
- "manager connection boundary reports success rather than triggering background reconnect"
|
||||
- "local-data/signer restore are virtualized; teardown awaits init, closes feed and resets reconnect state even after failure"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "early-return init mutant fails while pool initialization is held"
|
||||
- "no-op subscription cannot satisfy the observed transport request"
|
||||
- "no-op cleanup cannot satisfy exact close count"
|
||||
- "failure fixture cannot hide a reconnect timer behind a passing assertion"
|
||||
- "retry/sleep/forced-exit masking: no timeout changes or forced exit"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
Original concurrent-promise, pool-ready and post-init no-throw assertions remain. The old empty connection result accidentally modeled failure; init correctly scheduled manager-owned retries that bypassed its client-level stub. The fixture now models successful init at the manager boundary and releases its feed subscription. This is harness cleanup, not a Nostr product change.
|
||||
|
||||
Mutants: a test-local historical early-return init variant fails the pending-barrier assertion (exit1, 1 failed); a connection-failure fixture variant fails the no-retry assertion (exit1, 1 failed). Both exit naturally and are reverted. No product file was edited for either mutant.
|
||||
|
||||
---
|
||||
|
||||
# client-reliability-3
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-last-probe-owner-disposal
|
||||
given: "five real DOM cards register through actual gridHealth/cardObserver; four probes are active and one queued"
|
||||
when: "the feed is disposed/removed and active probes finish"
|
||||
then: "zero obsolete probes start"
|
||||
- id: SCN-shared-queued-owner
|
||||
given: "two cards share one queued torrent source"
|
||||
when: "one registration disposes while its DOM remains connected"
|
||||
then: "the live owner still probes and becomes healthy; disposed owner receives no stale UI/callback"
|
||||
- id: SCN-cancelled-replacement-inflight
|
||||
given: "last owner cancels queued work then a replacement requests the same source before old settlement"
|
||||
when: "old completion drains and another owner joins before a slot frees"
|
||||
then: "no unhealthy cancellation cache is written, only one new probe starts, and both replacements become healthy/cacheable"
|
||||
- id: SCN-probe-priority-concurrency
|
||||
given: "four active probes, queued mixed priorities and a cancelled source"
|
||||
when: "slots free one at a time"
|
||||
then: "four-slot maximum and stable priority order remain; cancelled work never starts"
|
||||
- id: SCN-hidden-healthy-reveal
|
||||
given: "a hide-all card is hidden pending liveness"
|
||||
when: "registration eagerly probes and returns healthy"
|
||||
then: "raw magnet/webseed bytes reach probe IO and the card reveals without an intersection"
|
||||
- id: SCN-active-shared-late-entry
|
||||
given: "an active source loses its owner, receives a late observer entry and gains a live owner"
|
||||
when: "the unchanged active probe completes"
|
||||
then: "no duplicate starts or disposed result writes; live owner becomes healthy"
|
||||
- id: SCN-repeat-disposal-reattach
|
||||
given: "a disposed container has a new queued registration"
|
||||
when: "the old disposer is called again"
|
||||
then: "new owner survives and becomes healthy"
|
||||
- id: SCN-ownerless-active-cache
|
||||
given: "an already active probe has no remaining owner"
|
||||
when: "it finishes before a later live card requests that source"
|
||||
then: "the active result remains cached; no new probe or stale UI write occurs"
|
||||
- id: SCN-existing-probe-cache-ttl
|
||||
given: "completed healthy and negative results"
|
||||
when: "cards request before, at, and after the existing TTL boundary"
|
||||
then: "both cached outcomes reuse until the existing inclusive TTL expires, then both probe again"
|
||||
observable_outcomes:
|
||||
- "actual probePeers start count/order, arguments and maximum active count"
|
||||
- "real DOM badge health/hidden state, observer disposal and callback counts"
|
||||
- "storage cache absence for cancellation and reuse/expiry for completed probes"
|
||||
determinism_controls:
|
||||
- "actual gridHealth/cardObserver and DOM, controllable observer and deferred probePeers IO"
|
||||
- "explicit viewport geometry, unique sources and isolated storage; fake Date.now only for TTL scenario"
|
||||
- "task-turn promise draining without wall-clock sleeps, polling or retries"
|
||||
- "native Chromium/Firefox observer smoke against built dist, external traffic blocked"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "unconditional queue cancellation: shared-owner mutant fails"
|
||||
- "no-op cancellation, stale inflight deletion or cancelled cache poisoning"
|
||||
- "changed concurrency/priority or hidden eager path"
|
||||
- "stale result/listener/observer effects and repeated old disposal"
|
||||
- "cancelling or dropping cache from already active work"
|
||||
- "timeout/retry/sleep masking or hard-coded healthy results"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
Grouped mutation coverage: last-owner cancellation mutant kills SCN-last-probe-owner-disposal; unconditional cancel kills SCN-shared-queued-owner; unconditional inflight deletion and cancelled-normalization mutants each kill SCN-cancelled-replacement-inflight; reversed queue priority kills SCN-probe-priority-concurrency; disabled eager path kills SCN-hidden-healthy-reveal; stale result guard and late-entry membership mutants each kill SCN-active-shared-late-entry; non-idempotent disposer kills SCN-repeat-disposal-reattach; owner-dependent active caching kills SCN-ownerless-active-cache; missing cache writes kills SCN-existing-probe-cache-ttl. All11 variants exit1 with1 failed scenario,0 cancelled, and are reverted. Browser checks corroborate the same disposal/shared-owner/hidden-reveal/source-visibility scenarios with native observers; no browser CPU or actual media fallback claim.
|
||||
|
||||
Additional client-reliability-3 scenarios (same invariant and controls):
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-shared-badge-listener-lifetime
|
||||
given: "two feed registrations share the app callback"
|
||||
when: "badge updates occur and registrations dispose one at a time"
|
||||
then: "each event notifies once, one surviving owner retains the callback, last disposal stops notifications"
|
||||
- id: SCN-checking-callback-disposal
|
||||
given: "a live card's checking callback synchronously disposes the feed"
|
||||
when: "badge notification returns to probe submission"
|
||||
then: "no probe is submitted after unregister"
|
||||
observable_outcomes:
|
||||
- "callback event count/state sequence and external probe start count"
|
||||
determinism_controls:
|
||||
- "real badge/observer path, synchronous disposal callback and deferred probe IO"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "duplicated shared callbacks or dropping live callback owners"
|
||||
- "post-disposal work submitted after synchronous reentrant teardown"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
Duplicate wrapper and unconditional listener release variants each fail SCN-shared-badge-listener-lifetime. Removing the post-checking membership guard fails SCN-checking-callback-disposal (actual starts1 vs expected0). These3 additional variants exit1 with1 failed/0 cancelled and are reverted; total14 killed variants for11 lifecycle scenarios. Original source/priority/observer expectations remain unchanged.
|
||||
|
||||
|
||||
## client-reliability-4 — queued URL media ownership
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-url-last-owner
|
||||
given: "four cards, three active media and one queued"
|
||||
when: "last registration is disposed and an active slot frees"
|
||||
then: "queued promise settles null, no cache entry or obsolete media start"
|
||||
- id: SCN-url-shared-owner
|
||||
given: "two feeds share a queued event/source"
|
||||
when: "one feed disposes"
|
||||
then: "exactly one media probe serves the live badge; disposed connected badge stays checking"
|
||||
- id: SCN-url-last-shared-owner
|
||||
given: "two feeds share a queued event/source"
|
||||
when: "both feeds dispose"
|
||||
then: "queued promise settles null and never starts media"
|
||||
- id: SCN-url-cancel-rerequest
|
||||
given: "cancelled queued event/source has pending promise continuations"
|
||||
when: "a new feed requests the same source immediately"
|
||||
then: "new inflight record survives old settlement and live badge becomes healthy"
|
||||
- id: SCN-url-cancel-mirror
|
||||
given: "queued primary has a mirror"
|
||||
when: "last owner disposes"
|
||||
then: "cancelled work settles without mirror HEAD/media or stale UI/cache"
|
||||
- id: SCN-url-registration-reuse
|
||||
given: "hidden old feed queues an eager callback"
|
||||
when: "dispose, reuse container, deliver old callback and old disposer"
|
||||
then: "only the new event reaches onCheck and starts IO"
|
||||
- id: SCN-url-active-cache
|
||||
given: "active media belongs to a connected card"
|
||||
when: "dispose its registration and complete active media"
|
||||
then: "active result caches healthy, disposed badge stays checking, new card uses cache"
|
||||
- id: SCN-url-hidden-reveal
|
||||
given: "URL-only card is hidden pending health"
|
||||
when: "eager media confirmation succeeds"
|
||||
then: "card becomes visible with healthy badge"
|
||||
- id: SCN-url-media-limit
|
||||
given: "three active media probes plus FIFO queued work"
|
||||
when: "slot releases while a new card enters"
|
||||
then: "existing queued source runs first; active media stays three"
|
||||
- id: SCN-url-mirror-order
|
||||
given: "primary and two mirrors exist"
|
||||
when: "primary errors then first mirror succeeds"
|
||||
then: "primary then first mirror are requested; second mirror is untouched"
|
||||
- id: SCN-url-legacy-callback
|
||||
given: "connected badge lacks observer ownership"
|
||||
when: "legacy caller invokes controller"
|
||||
then: "media settles and healthy badge paints"
|
||||
- id: SCN-url-cross-controller
|
||||
given: "shared real inflight cache and two controllers"
|
||||
when: "creator feed disposes while other feed remains"
|
||||
then: "queued shared source starts once and other controller paints healthy"
|
||||
observable_outcomes:
|
||||
- "HEAD and native media IO starts, promise settlement, real cache/inflight records, connected badge and card visibility"
|
||||
- "Observer callback emissions after disposal and container reuse"
|
||||
determinism_controls:
|
||||
- "jsdom cards, controllable IntersectionObserver, explicit loadeddata/error events and stubbed fetch boundary; real observer/controller/cache"
|
||||
- "Unique event/source keys and task-turn drain; no live traffic, arbitrary sleeps, retries or relaxed assertions"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "hard-coded healthy or cancelled results"
|
||||
- "testing a copied queue instead of real controller and observer"
|
||||
- "cancelling shared live work or caching cancellation as offline"
|
||||
- "stale connected DOM callbacks hidden by isConnected guards"
|
||||
- "retry/sleep-based flake masking"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
All existing assertions preserved. `tests/url-health-lifecycle.test.mjs` adds12 scenarios. Mutations are temporary edits restored before gates. `python3 /tmp/item4-mutants.py` final run:13/13 variants naturally exit1 with named assertion failures (no timeouts); logs `/tmp/item4-mutant-*.log`, results `/tmp/item4-mutants.json`. No production test runner/gate changes.
|
||||
|
||||
Coverage grouping: no-unregister→last owner; cancel-shared→shared owner/cross-controller; reuse-cancelled-inflight→immediate retry/new inflight; cache-cancellation→last/shared owner/retry/mirror cache outcomes; mirror-fallthrough→cancelled primary; grouped stale guards→shared/active disposed UI (remove both redundant owner guards together; two equivalent variants); eager-callback-stale→registration reuse; no-eager-reveal→hidden reveal; concurrency-four→three-media cap/FIFO; reverse-mirrors→mirror ordering; break-legacy-consumer→callback compatibility; controller-local-records→shared cache ownership across controllers. Initial mutation pass exposed callback-emission and first-source gaps; new assertions were strengthened, then all mutants rejected. Initial no-unregister full-file run failed assertions but hung in another scenario awaiting its intentionally broken promise; final targeted last-owner variant naturally exits1. No timeout is counted as successful mutant evidence.
|
||||
@@ -0,0 +1,363 @@
|
||||
// Per-video OpenGraph / Twitter-player rendering for crawlers.
|
||||
//
|
||||
// bitvid is a static SPA: index.html carries one set of site-wide OG tags, so
|
||||
// every `/?v=<nevent>` link previewed as the same generic bitvid card. Crawlers
|
||||
// don't run JS, so the client can't fix that — this module renders the tags a
|
||||
// crawler needs, and a thin host adapter (api/og.js on Vercel) feeds it.
|
||||
//
|
||||
// PURE by design: no network, no host globals. All I/O lives in the adapter so
|
||||
// the escaping and gating rules below stay unit-testable.
|
||||
//
|
||||
// Rollback: remove the crawler rewrite from vercel.json. The static site keeps
|
||||
// serving its existing tags and nothing else changes — this is never on the
|
||||
// path of a real user's page load, and `npm run archive` (nsite) is unaffected.
|
||||
|
||||
// Player card dimensions. Twitter refuses a player card without explicit
|
||||
// dimensions, and 16:9 matches bitvid's embed shell.
|
||||
export const DEFAULT_PLAYER_WIDTH = 1280;
|
||||
export const DEFAULT_PLAYER_HEIGHT = 720;
|
||||
|
||||
const MAX_TITLE_LENGTH = 120;
|
||||
const MAX_DESCRIPTION_LENGTH = 200;
|
||||
|
||||
/**
|
||||
* Escape a value for use inside a double-quoted HTML attribute.
|
||||
*
|
||||
* Titles and descriptions come from arbitrary nostr events — treat every one as
|
||||
* hostile. A title of `"><script>…` would otherwise break out of the meta tag
|
||||
* and inject markup into a page we serve from our own origin.
|
||||
*/
|
||||
export function escapeHtmlAttribute(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Only http(s) URLs may reach a meta tag. Anything else (javascript:, data:,
|
||||
* relative paths) is dropped — a crawler resolving them is at best useless and
|
||||
* at worst a vector.
|
||||
*/
|
||||
export function sanitizeAbsoluteUrl(value) {
|
||||
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return "";
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
/** Collapse whitespace and hard-truncate for preview surfaces. */
|
||||
export function normalizeText(value, maxLength) {
|
||||
const collapsed =
|
||||
typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
||||
if (!collapsed || !Number.isFinite(maxLength) || maxLength <= 0) {
|
||||
return collapsed;
|
||||
}
|
||||
if (collapsed.length <= maxLength) {
|
||||
return collapsed;
|
||||
}
|
||||
return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a video may be described in a link preview.
|
||||
*
|
||||
* A crawler never authenticates, so anything emitted here is fully public and
|
||||
* outlives us in third-party caches. The rule is fail-closed: only a video that
|
||||
* is affirmatively public gets a rich card; everything else — including the
|
||||
* "we couldn't determine it" case — falls back to the generic bitvid card.
|
||||
*
|
||||
* @returns {{allowed: boolean, reason: string}}
|
||||
*/
|
||||
export function isPubliclyShareable(video, options = {}) {
|
||||
if (!video || typeof video !== "object") {
|
||||
return { allowed: false, reason: "missing-video" };
|
||||
}
|
||||
if (video.invalid) {
|
||||
return { allowed: false, reason: "invalid-video" };
|
||||
}
|
||||
if (video.deleted === true) {
|
||||
return { allowed: false, reason: "deleted" };
|
||||
}
|
||||
if (video.isPrivate === true) {
|
||||
return { allowed: false, reason: "private" };
|
||||
}
|
||||
// NSFW thumbnails must never be injected into an unsuspecting timeline.
|
||||
if (video.isNsfw === true) {
|
||||
return { allowed: false, reason: "nsfw" };
|
||||
}
|
||||
|
||||
const pubkey =
|
||||
typeof video.pubkey === "string" ? video.pubkey.trim().toLowerCase() : "";
|
||||
if (!pubkey) {
|
||||
return { allowed: false, reason: "missing-author" };
|
||||
}
|
||||
|
||||
const blacklist = options.blacklist instanceof Set ? options.blacklist : null;
|
||||
if (blacklist && blacklist.has(pubkey)) {
|
||||
return { allowed: false, reason: "blacklisted" };
|
||||
}
|
||||
|
||||
if (options.whitelistEnabled) {
|
||||
const whitelist =
|
||||
options.whitelist instanceof Set ? options.whitelist : null;
|
||||
// No usable whitelist while whitelist mode is on means we cannot prove the
|
||||
// author is allowed. Fail closed rather than leak a gated video's metadata.
|
||||
if (!whitelist || whitelist.size === 0) {
|
||||
return { allowed: false, reason: "whitelist-unavailable" };
|
||||
}
|
||||
if (!whitelist.has(pubkey)) {
|
||||
return { allowed: false, reason: "not-whitelisted" };
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true, reason: "ok" };
|
||||
}
|
||||
|
||||
// bitvid's own notes are kind 30078 with a v3 JSON body. Other clients
|
||||
// (nostube/slidestr and anything NIP-71 native) publish kind 21/22/34235/34236
|
||||
// with the metadata in TAGS and the description in `content`. bitvid normalizes
|
||||
// both in the browser, so the preview must understand both too — parsing only
|
||||
// the JSON shape made every NIP-71 video fall back to the generic card.
|
||||
export const NIP71_VIDEO_KINDS = Object.freeze([21, 22, 34235, 34236]);
|
||||
|
||||
function firstTagValue(tags, name) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (Array.isArray(tag) && tag[0] === name && typeof tag[1] === "string") {
|
||||
const value = tag[1].trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function hasTag(tags, name) {
|
||||
return (
|
||||
Array.isArray(tags) &&
|
||||
tags.some((tag) => Array.isArray(tag) && tag[0] === name)
|
||||
);
|
||||
}
|
||||
|
||||
/** First `image <url>` entry inside any imeta tag. */
|
||||
function imetaImageUrl(tags) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return "";
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if (!Array.isArray(tag) || tag[0] !== "imeta") {
|
||||
continue;
|
||||
}
|
||||
for (let i = 1; i < tag.length; i += 1) {
|
||||
const entry = typeof tag[i] === "string" ? tag[i].trim() : "";
|
||||
if (/^image\s+/i.test(entry)) {
|
||||
const url = sanitizeAbsoluteUrl(entry.replace(/^image\s+/i, ""));
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a raw nostr event into the fields the preview needs, for both the
|
||||
* bitvid v3 JSON shape and the NIP-71 tag shape. Returns null when the event is
|
||||
* not a recognizable video note (caller renders the generic card).
|
||||
*/
|
||||
export function parseVideoEvent(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base = { pubkey: event.pubkey, deleted: false, isPrivate: false };
|
||||
|
||||
// --- bitvid native (kind 30078, JSON content) ---
|
||||
if (typeof event.content === "string" && event.content.trim().startsWith("{")) {
|
||||
try {
|
||||
const content = JSON.parse(event.content);
|
||||
if (content && typeof content === "object") {
|
||||
return {
|
||||
...base,
|
||||
title: content.title,
|
||||
description: content.description,
|
||||
thumbnail: content.thumbnail,
|
||||
deleted: content.deleted === true,
|
||||
isPrivate: content.isPrivate === true,
|
||||
isNsfw: content.isNsfw === true || hasTag(event.tags, "content-warning"),
|
||||
invalid: !content.title || (!content.url && !content.magnet),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Not JSON after all — fall through to the NIP-71 reading.
|
||||
}
|
||||
}
|
||||
|
||||
// --- NIP-71 (metadata in tags, description in content) ---
|
||||
if (NIP71_VIDEO_KINDS.includes(event.kind)) {
|
||||
const title = firstTagValue(event.tags, "title");
|
||||
const thumbnail =
|
||||
imetaImageUrl(event.tags) ||
|
||||
sanitizeAbsoluteUrl(firstTagValue(event.tags, "image")) ||
|
||||
sanitizeAbsoluteUrl(firstTagValue(event.tags, "thumb"));
|
||||
const description =
|
||||
(typeof event.content === "string" && event.content.trim()) ||
|
||||
firstTagValue(event.tags, "summary") ||
|
||||
firstTagValue(event.tags, "alt");
|
||||
return {
|
||||
...base,
|
||||
title,
|
||||
description,
|
||||
thumbnail,
|
||||
// NIP-71 has no privacy flag; `content-warning` is the NSFW signal.
|
||||
isNsfw: hasTag(event.tags, "content-warning"),
|
||||
invalid: !title,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function metaTag(attribute, name, content) {
|
||||
return ` <meta ${attribute}="${escapeHtmlAttribute(name)}" content="${escapeHtmlAttribute(content)}" />`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the crawler-facing HTML document.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {object|null} params.video Normalized video object, or null.
|
||||
* @param {string} params.shareUrl Canonical `/?v=<nevent>` URL.
|
||||
* @param {string} [params.embedUrl] `/embed.html?pointer=<nevent>` player URL.
|
||||
* @param {object} params.fallback Site-wide defaults (title/description/image/url).
|
||||
* @param {object} [params.gate] Result of isPubliclyShareable().
|
||||
* @returns {string} A complete HTML document.
|
||||
*/
|
||||
export function renderOgHtml({
|
||||
video = null,
|
||||
shareUrl = "",
|
||||
embedUrl = "",
|
||||
fallback = {},
|
||||
gate = null,
|
||||
} = {}) {
|
||||
const siteName = normalizeText(fallback.siteName || "bitvid", 60);
|
||||
const fallbackTitle = normalizeText(
|
||||
fallback.title || "bitvid - Decentralized Video Sharing",
|
||||
MAX_TITLE_LENGTH
|
||||
);
|
||||
const fallbackDescription = normalizeText(
|
||||
fallback.description || "Decentralized video sharing on Nostr.",
|
||||
MAX_DESCRIPTION_LENGTH
|
||||
);
|
||||
const fallbackImage = sanitizeAbsoluteUrl(fallback.image);
|
||||
const canonical = sanitizeAbsoluteUrl(shareUrl) || sanitizeAbsoluteUrl(fallback.url);
|
||||
|
||||
const decision = gate || isPubliclyShareable(video);
|
||||
const useVideo = decision.allowed === true;
|
||||
|
||||
const title = useVideo
|
||||
? normalizeText(video.title, MAX_TITLE_LENGTH) || fallbackTitle
|
||||
: fallbackTitle;
|
||||
const description = useVideo
|
||||
? normalizeText(video.description, MAX_DESCRIPTION_LENGTH) ||
|
||||
fallbackDescription
|
||||
: fallbackDescription;
|
||||
const image = useVideo
|
||||
? sanitizeAbsoluteUrl(video.thumbnail) || fallbackImage
|
||||
: fallbackImage;
|
||||
const player = useVideo ? sanitizeAbsoluteUrl(embedUrl) : "";
|
||||
|
||||
const lines = [
|
||||
"<!doctype html>",
|
||||
'<html lang="en">',
|
||||
" <head>",
|
||||
' <meta charset="utf-8" />',
|
||||
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
||||
` <title>${escapeHtmlAttribute(title)}</title>`,
|
||||
];
|
||||
|
||||
if (canonical) {
|
||||
lines.push(` <link rel="canonical" href="${escapeHtmlAttribute(canonical)}" />`);
|
||||
}
|
||||
|
||||
lines.push(metaTag("name", "description", description));
|
||||
lines.push(metaTag("property", "og:site_name", siteName));
|
||||
lines.push(metaTag("property", "og:type", useVideo ? "video.other" : "website"));
|
||||
lines.push(metaTag("property", "og:title", title));
|
||||
lines.push(metaTag("property", "og:description", description));
|
||||
if (canonical) {
|
||||
lines.push(metaTag("property", "og:url", canonical));
|
||||
}
|
||||
if (image) {
|
||||
lines.push(metaTag("property", "og:image", image));
|
||||
lines.push(metaTag("property", "og:image:alt", title));
|
||||
}
|
||||
|
||||
if (player) {
|
||||
// A player card is what makes a shared bitvid link behave like a YouTube
|
||||
// link: the video plays inline in the timeline instead of showing a still.
|
||||
lines.push(metaTag("property", "og:video:url", player));
|
||||
lines.push(metaTag("property", "og:video:secure_url", player));
|
||||
lines.push(metaTag("property", "og:video:type", "text/html"));
|
||||
lines.push(metaTag("property", "og:video:width", String(DEFAULT_PLAYER_WIDTH)));
|
||||
lines.push(metaTag("property", "og:video:height", String(DEFAULT_PLAYER_HEIGHT)));
|
||||
lines.push(metaTag("name", "twitter:card", "player"));
|
||||
lines.push(metaTag("name", "twitter:player", player));
|
||||
lines.push(metaTag("name", "twitter:player:width", String(DEFAULT_PLAYER_WIDTH)));
|
||||
lines.push(metaTag("name", "twitter:player:height", String(DEFAULT_PLAYER_HEIGHT)));
|
||||
} else {
|
||||
lines.push(metaTag("name", "twitter:card", image ? "summary_large_image" : "summary"));
|
||||
}
|
||||
|
||||
lines.push(metaTag("name", "twitter:title", title));
|
||||
lines.push(metaTag("name", "twitter:description", description));
|
||||
if (image) {
|
||||
lines.push(metaTag("name", "twitter:image", image));
|
||||
}
|
||||
|
||||
lines.push(" </head>");
|
||||
lines.push(" <body>");
|
||||
// A human should never land here (only crawler user-agents are rewritten),
|
||||
// but if one does, give them a real link rather than a blank page.
|
||||
lines.push(
|
||||
canonical
|
||||
? ` <p><a href="${escapeHtmlAttribute(canonical)}">${escapeHtmlAttribute(title)}</a></p>`
|
||||
: ` <p>${escapeHtmlAttribute(title)}</p>`
|
||||
);
|
||||
lines.push(" </body>");
|
||||
lines.push("</html>");
|
||||
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
// Conservative crawler match. Being wrong in the "not a bot" direction is free
|
||||
// (the visitor gets the normal SPA); being wrong the other way would serve a
|
||||
// human this stub page, so the pattern stays narrow and explicit.
|
||||
export const CRAWLER_USER_AGENT_PATTERN =
|
||||
/(facebookexternalhit|twitterbot|slackbot|discordbot|telegrambot|whatsapp|linkedinbot|pinterest|redditbot|embedly|quora link preview|nostrbot|primal|iframely|opengraph|mastodon|bluesky|skypeuripreview|vkshare|googlebot|bingbot|applebot|yandexbot|duckduckbot|baiduspider|petalbot|semrushbot|ahrefsbot|developers\.google\.com\/\+\/web\/snippet)/i;
|
||||
|
||||
export function isCrawlerUserAgent(userAgent) {
|
||||
return typeof userAgent === "string"
|
||||
? CRAWLER_USER_AGENT_PATTERN.test(userAgent)
|
||||
: false;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Vercel serverless handler: renders per-video OpenGraph / Twitter-player tags
|
||||
// for crawlers hitting `/?v=<nevent>`.
|
||||
//
|
||||
// Nothing is pre-generated and nothing is persisted. This is a stateless
|
||||
// function invoked per crawler request; `dist/` is byte-identical with or
|
||||
// without it. The only caching is the CDN edge cache via Cache-Control below.
|
||||
//
|
||||
// This must never become load-bearing for the app: real users are not routed
|
||||
// here (see the user-agent rewrite in vercel.json), and a static/nsite deploy
|
||||
// without any functions keeps working exactly as it does today, just with the
|
||||
// site-wide card.
|
||||
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { SimplePool, useWebSocketImplementation } from "nostr-tools/pool";
|
||||
import WebSocket from "ws";
|
||||
|
||||
import {
|
||||
isCrawlerUserAgent,
|
||||
isPubliclyShareable,
|
||||
parseVideoEvent,
|
||||
renderOgHtml,
|
||||
} from "./_lib/ogRenderer.mjs";
|
||||
|
||||
// Imported, not duplicated: these modules are plain ESM with no DOM access, so
|
||||
// the function reads the SAME admin-list identity and relay set the browser
|
||||
// does. A hardcoded copy would silently drift the moment an operator retunes
|
||||
// the instance, and the failure mode (previews gated forever, or worse, a stale
|
||||
// whitelist) would be invisible.
|
||||
import {
|
||||
ADMIN_LIST_NAMESPACE,
|
||||
ADMIN_SUPER_NPUB,
|
||||
} from "../js/config.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../js/nostr/toolkit.js";
|
||||
import { ADMIN_LIST_IDENTIFIERS } from "../js/nostrEventSchemas.js";
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
const RELAYS = Array.from(DEFAULT_RELAY_URLS);
|
||||
|
||||
// Admin moderation lists are addressable kind-30000 sets published by the
|
||||
// instance's super admin (js/adminListStore.js).
|
||||
const ADMIN_LIST_KIND = 30000;
|
||||
|
||||
// Measured: a COLD pool's first read across these relays can take ~3s (WSS
|
||||
// handshake + REQ round trip on four connections). 1.5s and 2.5s both cut it
|
||||
// off and served the generic card every time. Twitter allows ~5s and Facebook
|
||||
// ~10s, so 4s buys a correct card on a cold container while still leaving
|
||||
// headroom under Vercel's function limit. Warm containers answer in <600ms.
|
||||
const RELAY_TIMEOUT_MS = 4000;
|
||||
|
||||
// Admin lists are identical for every request, so re-reading them on each crawl
|
||||
// is pure waste (and it was starving the video read of its budget). Memoized in
|
||||
// module scope: warm invocations skip the relay round trip entirely. This is a
|
||||
// per-container, in-memory cache of PUBLIC data that dies with the container —
|
||||
// nothing is persisted anywhere.
|
||||
const ADMIN_LIST_TTL_MS = 5 * 60 * 1000;
|
||||
let adminListCache = { expiresAt: 0, whitelist: null, blacklist: null };
|
||||
|
||||
const SITE_ORIGIN = process.env.BITVID_SITE_ORIGIN || "https://bitvid.network";
|
||||
const FALLBACK = Object.freeze({
|
||||
siteName: "bitvid",
|
||||
title: "bitvid - Decentralized Video Sharing",
|
||||
description:
|
||||
"bitvid is a decentralized video sharing platform built on Nostr and WebTorrent.",
|
||||
image: `${SITE_ORIGIN}/assets/jpg/bitvid.jpg`,
|
||||
url: SITE_ORIGIN,
|
||||
});
|
||||
|
||||
function withTimeout(promise, ms, fallbackValue) {
|
||||
return Promise.race([
|
||||
promise.catch(() => fallbackValue),
|
||||
new Promise((resolve) => setTimeout(() => resolve(fallbackValue), ms)),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Decode `?v=` (nevent/naddr/note/raw hex) into a relay filter. */
|
||||
function pointerToFilter(pointer) {
|
||||
const trimmed = typeof pointer === "string" ? pointer.trim() : "";
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
||||
return { ids: [trimmed.toLowerCase()] };
|
||||
}
|
||||
try {
|
||||
const decoded = nip19.decode(trimmed);
|
||||
if (decoded.type === "nevent" && decoded.data?.id) {
|
||||
return { ids: [decoded.data.id] };
|
||||
}
|
||||
if (decoded.type === "note" && typeof decoded.data === "string") {
|
||||
return { ids: [decoded.data] };
|
||||
}
|
||||
if (decoded.type === "naddr" && decoded.data?.identifier) {
|
||||
return {
|
||||
kinds: [decoded.data.kind],
|
||||
authors: [decoded.data.pubkey],
|
||||
"#d": [decoded.data.identifier],
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// Unparseable pointer -> generic card.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractPubkeySet(event) {
|
||||
const out = new Set();
|
||||
if (!event || !Array.isArray(event.tags)) {
|
||||
return out;
|
||||
}
|
||||
for (const tag of event.tags) {
|
||||
if (!Array.isArray(tag) || tag[0] !== "p" || typeof tag[1] !== "string") {
|
||||
continue;
|
||||
}
|
||||
const value = tag[1].trim().toLowerCase();
|
||||
if (/^[0-9a-f]{64}$/.test(value)) {
|
||||
out.add(value);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const decoded = nip19.decode(value);
|
||||
if (decoded.type === "npub" && typeof decoded.data === "string") {
|
||||
out.add(decoded.data.toLowerCase());
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore malformed entries
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const host = req.headers?.host || new URL(SITE_ORIGIN).host;
|
||||
const proto = req.headers?.["x-forwarded-proto"] || "https";
|
||||
const requestUrl = new URL(req.url || "/", `${proto}://${host}`);
|
||||
const pointer = requestUrl.searchParams.get("v") || "";
|
||||
|
||||
// Derive the origin from the REQUEST, not from a constant. Otherwise every
|
||||
// non-production deployment (unstable, previews) advertises production's
|
||||
// canonical URL and — worse — a twitter:player pointing at production's
|
||||
// embed, so testing a preview link silently exercises the live site.
|
||||
const origin = `${proto}://${host}`;
|
||||
|
||||
const shareUrl = pointer
|
||||
? `${origin}/?v=${encodeURIComponent(pointer)}`
|
||||
: origin;
|
||||
const embedUrl = pointer
|
||||
? `${origin}/embed.html?pointer=${encodeURIComponent(pointer)}`
|
||||
: "";
|
||||
|
||||
const renderFallback = (reason) => {
|
||||
// Short TTL: a video may be published, edited or moderated at any time, and
|
||||
// we would rather re-ask the relays than pin a wrong card in someone's CDN.
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");
|
||||
res.setHeader("X-Bitvid-OG", reason);
|
||||
res.status(200).send(
|
||||
renderOgHtml({
|
||||
video: null,
|
||||
shareUrl,
|
||||
fallback: { ...FALLBACK, url: origin, image: `${origin}/assets/jpg/bitvid.jpg` },
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Defensive: the rewrite should only send crawlers here, but never serve a
|
||||
// human this stub if the rule ever widens.
|
||||
if (!isCrawlerUserAgent(req.headers?.["user-agent"])) {
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.redirect(302, shareUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const filter = pointerToFilter(pointer);
|
||||
if (!filter) {
|
||||
renderFallback("no-pointer");
|
||||
return;
|
||||
}
|
||||
|
||||
const pool = new SimplePool();
|
||||
let superAdminHex = "";
|
||||
try {
|
||||
const decoded = nip19.decode(ADMIN_SUPER_NPUB);
|
||||
superAdminHex = decoded.type === "npub" ? decoded.data : "";
|
||||
} catch (err) {
|
||||
superAdminHex = "";
|
||||
}
|
||||
|
||||
try {
|
||||
const listFilter = (identifier) => ({
|
||||
kinds: [ADMIN_LIST_KIND],
|
||||
"#d": [`${ADMIN_LIST_NAMESPACE}:${identifier}`],
|
||||
...(superAdminHex ? { authors: [superAdminHex] } : {}),
|
||||
});
|
||||
|
||||
// One shared deadline across all three reads.
|
||||
const now = Date.now();
|
||||
const adminListsFresh =
|
||||
adminListCache.expiresAt > now && adminListCache.whitelist instanceof Set;
|
||||
|
||||
const [videoEvent, whitelistEvent, blacklistEvent] = await Promise.all([
|
||||
withTimeout(pool.get(RELAYS, filter), RELAY_TIMEOUT_MS, null),
|
||||
adminListsFresh
|
||||
? Promise.resolve(null)
|
||||
: withTimeout(
|
||||
pool.get(RELAYS, listFilter(ADMIN_LIST_IDENTIFIERS.whitelist)),
|
||||
RELAY_TIMEOUT_MS,
|
||||
null
|
||||
),
|
||||
adminListsFresh
|
||||
? Promise.resolve(null)
|
||||
: withTimeout(
|
||||
pool.get(RELAYS, listFilter(ADMIN_LIST_IDENTIFIERS.blacklist)),
|
||||
RELAY_TIMEOUT_MS,
|
||||
null
|
||||
),
|
||||
]);
|
||||
|
||||
let whitelist = adminListsFresh ? adminListCache.whitelist : null;
|
||||
let blacklist = adminListsFresh ? adminListCache.blacklist : null;
|
||||
if (!adminListsFresh) {
|
||||
whitelist = extractPubkeySet(whitelistEvent);
|
||||
blacklist = extractPubkeySet(blacklistEvent);
|
||||
// Only cache a whitelist we actually got. Caching an empty one would pin
|
||||
// "whitelist-unavailable" for the whole TTL after a single slow read.
|
||||
if (whitelist.size > 0) {
|
||||
adminListCache = {
|
||||
expiresAt: now + ADMIN_LIST_TTL_MS,
|
||||
whitelist,
|
||||
blacklist,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const video = parseVideoEvent(videoEvent);
|
||||
if (!video) {
|
||||
renderFallback("event-not-found");
|
||||
return;
|
||||
}
|
||||
|
||||
const gate = isPubliclyShareable(video, {
|
||||
whitelistEnabled: true,
|
||||
whitelist,
|
||||
blacklist,
|
||||
});
|
||||
|
||||
if (!gate.allowed) {
|
||||
renderFallback(`gated:${gate.reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");
|
||||
res.setHeader("X-Bitvid-OG", "video");
|
||||
res.status(200).send(
|
||||
renderOgHtml({
|
||||
video,
|
||||
shareUrl,
|
||||
embedUrl,
|
||||
fallback: { ...FALLBACK, url: origin, image: `${origin}/assets/jpg/bitvid.jpg` },
|
||||
gate,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
renderFallback("error");
|
||||
} finally {
|
||||
try {
|
||||
pool.close(RELAYS);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,18 +47,48 @@
|
||||
>
|
||||
<div class="space-y-2 text-left">
|
||||
<p class="text-sm text-text-muted">
|
||||
Choose how you'd like to sign in. Connect a browser extension, link a
|
||||
remote signer, or work directly with your keys when you need to.
|
||||
Use BitLogin for a password-style sign-in, or connect the Nostr
|
||||
identity you already use. Both keep you in control of your keys.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Permanent, statically-declared mount for <bitlogin-auth> -- never
|
||||
written by an innerHTML re-render (this markup never changes after
|
||||
the page loads), so the element is only ever hidden/shown, never
|
||||
destroyed and recreated. Removing it from the DOM tears down its
|
||||
crypto Web Worker (see BitLogin's README).
|
||||
-->
|
||||
<div
|
||||
id="bitloginMount"
|
||||
class="rounded-3xl border border-border bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
|
||||
>
|
||||
<div class="mb-3 space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">BitLogin</h3>
|
||||
<p class="text-sm text-text-muted">
|
||||
A portable login name and password for BitLogin-enabled sites.
|
||||
</p>
|
||||
</div>
|
||||
<bitlogin-auth
|
||||
id="bitloginWidget"
|
||||
vault-relays="wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band,wss://nostr.wine,wss://relay.snort.social"
|
||||
discovery-relays="wss://purplepag.es,wss://relay.nostr.band,wss://nostr-pub.wellorder.net"
|
||||
></bitlogin-auth>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="space-y-5 rounded-3xl border border-border bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
|
||||
class="space-y-5 rounded-3xl border border-accent/40 bg-overlay-panel-soft/90 p-4 shadow-lg backdrop-blur-sm sm:p-5"
|
||||
aria-labelledby="nostrLoginOptionsTitle"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-text">Available methods</h3>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-accent">
|
||||
Nostr sign-in
|
||||
</p>
|
||||
<h3 id="nostrLoginOptionsTitle" class="text-base font-semibold text-text">
|
||||
Use your existing Nostr identity
|
||||
</h3>
|
||||
<p class="text-sm text-text-muted">
|
||||
Pick the option that matches how you manage your Nostr identity.
|
||||
Browser extension (NIP-07), remote signer (NIP-46), or your own key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -100,7 +100,15 @@
|
||||
>
|
||||
<!-- Left navigation column -->
|
||||
<div class="profile-modal__menu" data-profile-mobile-menu aria-hidden="false">
|
||||
<nav class="profile-modal__nav" aria-label="Profile sections">
|
||||
<div class="profile-modal__menu-heading">
|
||||
<h3 id="profileModalMenuHeading" class="profile-modal__menu-title">
|
||||
Profile & settings
|
||||
</h3>
|
||||
<p class="profile-modal__menu-hint">
|
||||
Choose what you want to manage
|
||||
</p>
|
||||
</div>
|
||||
<nav class="profile-modal__nav" aria-labelledby="profileModalMenuHeading">
|
||||
<button
|
||||
id="profileNavAccount"
|
||||
type="button"
|
||||
@@ -108,6 +116,7 @@
|
||||
aria-controls="profilePaneAccount"
|
||||
aria-selected="true"
|
||||
data-state="active"
|
||||
data-mobile-description="Profile and account settings"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -130,6 +139,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneRelays"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Manage your relay connections"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -152,6 +162,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneWallet"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Manage your wallet and payments"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -173,6 +184,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneStorage"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Manage your stored files"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -195,6 +207,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneMyVideos"
|
||||
aria-selected="false"
|
||||
data-mobile-description="View and manage your videos"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -217,6 +230,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePanePlaylists"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Organize your playlists"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -239,6 +253,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneHashtags"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Explore and follow hashtags"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -261,6 +276,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneSubscriptions"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Manage your subscriptions"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -286,6 +302,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneFriends"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Find and manage your friends"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -308,6 +325,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneSafety"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Privacy and safety settings"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -330,6 +348,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneBlocked"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Manage blocked users"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -352,6 +371,7 @@
|
||||
class="btn-ghost focus-ring relative w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneMessages"
|
||||
aria-selected="false"
|
||||
data-mobile-description="View your messages"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -379,6 +399,7 @@
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneHistory"
|
||||
aria-selected="false"
|
||||
data-mobile-description="View your watch history"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -408,6 +429,7 @@
|
||||
class="btn-ghost focus-ring hidden w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneAdmin"
|
||||
aria-selected="false"
|
||||
data-mobile-description="Moderate users and content"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
@@ -429,10 +451,12 @@
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="profile-modal__mobile-actions" role="group" aria-label="Session actions">
|
||||
<button
|
||||
id="profileMobileLockNowBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium mt-6 lg:hidden hidden"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium lg:hidden hidden"
|
||||
title="Forget your unlocked key on this device and re-lock storage + wallet now"
|
||||
>
|
||||
<svg
|
||||
@@ -453,7 +477,7 @@
|
||||
<button
|
||||
id="profileMobileLogoutBtn"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium mt-2 lg:hidden"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium lg:hidden"
|
||||
data-variant="critical"
|
||||
>
|
||||
<svg
|
||||
@@ -471,7 +495,7 @@
|
||||
</svg>
|
||||
Logout
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right content panes -->
|
||||
@@ -1179,7 +1203,7 @@ https://another.blossom.example"></textarea>
|
||||
></p>
|
||||
<label id="profileMyVideosAutoShareRow" class="hidden flex items-start gap-2 text-sm text-muted-strong">
|
||||
<input id="profileMyVideosAutoShare" type="checkbox" class="checkbox mt-0.5" />
|
||||
<span>Auto-share new public videos to other Nostr apps (NIP-71)</span>
|
||||
<span>Mirror new public videos to other Nostr apps by default (NIP-71)</span>
|
||||
</label>
|
||||
<p
|
||||
id="profileMyVideosLoading"
|
||||
|
||||
@@ -309,6 +309,10 @@
|
||||
<input type="checkbox" id="check-kids" class="checkbox" />
|
||||
<span class="text-sm text-text">For Kids</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="check-no-nip71-mirror" class="checkbox" />
|
||||
<span class="text-sm text-text">Do not share this video to other Nostr apps</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options -->
|
||||
|
||||
@@ -276,6 +276,20 @@ export const FEATURE_AUDIO_INGEST = false;
|
||||
*/
|
||||
export const FEATURE_BITCOIN_CONNECT = true;
|
||||
|
||||
/**
|
||||
* Whether the login modal offers BitLogin (a portable Nostr identity behind a
|
||||
* login name/password, https://github.com/PR0M3TH3AN/bitlogin) as the primary
|
||||
* sign-in method, with the NIP-07 extension, nsec, and NIP-46 options
|
||||
* collapsed under "Other login options" (js/ui/bitloginModalIntegration.js).
|
||||
* The vendored widget bundle (vendor/bitlogin/, scripts/build-bitlogin-widget.mjs)
|
||||
* only lazy-loads once the login modal's HTML is present in the DOM.
|
||||
*
|
||||
* Enabled on unstable for the final production-relay smoke test. Keep this
|
||||
* gated from beta/main until that manual login, reload, logout, and recovery
|
||||
* pass is confirmed.
|
||||
*/
|
||||
export const FEATURE_BITLOGIN = true;
|
||||
|
||||
/**
|
||||
* Whether this deployment offers Blossom (nostr-native blob storage) as a
|
||||
* first-class upload provider alongside Cloudflare R2 / generic S3 / Backblaze B2
|
||||
@@ -359,9 +373,9 @@ export const DEFAULT_TRUST_SEED_NPUBS = [
|
||||
* When a video receives at least this many trusted reports for the active
|
||||
* category (for example, `nudity`), thumbnails render with the blurred preview
|
||||
* by default. Align this value with your moderation policy — the upstream
|
||||
* deployment targets 1 trusted report before blurring.
|
||||
* deployment requires 3 trusted reports before blurring.
|
||||
*/
|
||||
export const DEFAULT_BLUR_THRESHOLD = 1;
|
||||
export const DEFAULT_BLUR_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* Baseline trusted-report count that blocks autoplay during browsing.
|
||||
@@ -369,9 +383,9 @@ export const DEFAULT_BLUR_THRESHOLD = 1;
|
||||
* Videos that meet or exceed this count will not autoplay in feeds unless the
|
||||
* viewer explicitly opts in. Increase the number for a more permissive stance
|
||||
* or decrease it if you want autoplay to stop sooner. The upstream deployment
|
||||
* targets 1 trusted report before blocking autoplay.
|
||||
* requires 3 trusted reports before blocking autoplay.
|
||||
*/
|
||||
export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 1;
|
||||
export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* Trusted mute threshold that hides creators globally.
|
||||
@@ -388,10 +402,10 @@ export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = 20;
|
||||
*
|
||||
* When a video's trusted report count reaches this number, bitvid treats it as
|
||||
* spam or policy violating content and removes it from default feeds. Align
|
||||
* the value with your moderation policy — 1 trusted report is the upstream
|
||||
* the value with your moderation policy — 5 trusted reports is the current
|
||||
* baseline.
|
||||
*/
|
||||
export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = 1;
|
||||
export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = 5;
|
||||
|
||||
/**
|
||||
* Optional theme accent overrides for light and dark mode.
|
||||
|
||||
@@ -56,7 +56,7 @@ To maintain a functional and ethical platform, the following types of content ar
|
||||
|
||||
bitvid enforces these guidelines through client-side visibility rules that match the current `moreMenuController` and `userBlocks` behavior:
|
||||
|
||||
- **Blurred previews:** When trusted NIP-56 `nudity` reports reach the configured blur threshold (`DEFAULT_BLUR_THRESHOLD` in [`config/instance-config.js`](../config/instance-config.js)), thumbnails render blurred and autoplay previews pause. Every blur includes a “show anyway” override. The upstream configuration currently uses one trusted report, but adjust the export to match your policy.
|
||||
- **Blurred previews:** When trusted NIP-56 `nudity` reports reach the configured blur threshold (`DEFAULT_BLUR_THRESHOLD` in [`config/instance-config.js`](../config/instance-config.js)), thumbnails render blurred and autoplay previews pause. Every blur includes a “show anyway” override. The hosted default requires three trusted reports.
|
||||
- **Downranked authors:** Muting a creator adds them to your NIP-51 mute list (kind `10000`). Videos from muted creators are deprioritized but still visible with clear badges so you can reverse the action.
|
||||
- **Personal blocks:** Blocking a creator adds them to your private block list (`userBlocks`). Videos from blocked creators are hidden from feeds, carousels, and modals unless you explicitly unblock them.
|
||||
- **Admin hard hides:** Moderator-maintained blacklists (NIP-51 kind `30000`) remove creators from default feeds for everyone using the hosted client. Moderators can still review the content through “show anyway” controls when auditing decisions.
|
||||
@@ -88,7 +88,7 @@ You can manage moderation directly from the client using the existing menus rend
|
||||
|
||||
### **5.5 Safety & Moderation thresholds**
|
||||
|
||||
- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The upstream repo ships example values of 1 trusted report for blur, 1 for autoplay blocking, 20 trusted mutes to hide authors, and 1 trusted spam report to hide videos—tune the config file before deployment if your community needs different baselines.
|
||||
- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the defaults exported from [`config/instance-config.js`](../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). The hosted defaults require 3 trusted reports for blur and autoplay blocking, 20 trusted mutes to hide authors, and 5 trusted spam reports to hide videos—tune the config file before deployment if your community needs different baselines.
|
||||
- These controls update immediately and sync with the moderation overlays applied by `moreMenuController` and `VideoCard` components. Operators can edit the config file to change the baseline before deploying their own instance.
|
||||
|
||||
## **6. Appeals and Feedback**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Goal: TODO backlog progress, bug fixes, robustness and usability. Scope: dispatch triage and proposed first team. Non-goals: product edits, worker launch before approval, deploys, spending, production data. Done: goal recorded, baseline measured, concrete first-team proposal presented.
|
||||
@@ -0,0 +1,3 @@
|
||||
- Goal: classify and fix profile-modal-controller suite hang at current HEAD 9e6c310d (unchanged from filedAt 8b204687).
|
||||
- Scope: owned test and narrow modal lifecycle helpers only. Preserve assertions; no retries, sleeps, timeout increases, quarantine or forced success exit.
|
||||
- Done: deterministic regression, failing mutant, isolated natural exit, full 60s/file unit run, lint; build/browser if product changes; fleet commit, merge unstable, checks, queue and durable landing.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Goal: make tests/race/nostr-client-init-race.test.mjs hermetic and naturally exit while preserving concurrent init, pool readiness, and post-init subscription coverage.
|
||||
- Temporary ownership granted by dispatch via landed RULE-CHANGES/prompt and owner resume instruction; no Nostr product/shared helper/global runner edits.
|
||||
- Revalidated unchanged from filedAt f303ab0e72; prior isolated assertion60.91ms passes but process timeout10s/exit124, full gate timeout60s at file227.
|
||||
- Plan: inspect NIP01 and real init/L1 subscription lifecycle; replace delay with deferred pool barrier; virtualize current pool boundary, close subscription; add observable boundary/cleanup assertions; mutant-check, run both isolated suites, commit separately, merge unstable, lint and full60s/file gates, land normally or route further blockers.
|
||||
- Preserve prior profile-modal commits and all assertions. No browser, real wallet, signer, external relay or deploy operations.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Goal: cancel unstarted torrent health probes when last feed/card owner unregisters, preserving live shared owners and active probes.
|
||||
- Scope: gridHealth.js, optional queue helper and focused tests/integrity/area notes. Excludes app, URL health, webtorrent implementation, playback/magnet helpers, cache TTLs/concurrency, runner/kit. Dispatch completion mailbox required; no scheduler.
|
||||
- Revalidated unchanged gridHealth/cardObserver from filedAtf9adfc8f through startup66832dc3. Adapted actual dispatch five-card reproduction to own imports:4 start,1 queues, dispose/remove then release ->1 obsolete start; assertion exit1.
|
||||
- Plan: introduce cancellable pending queue records and per-card leases; release at existing onCardUnregister; preserve active completion and cache, identity-check inflight deletion; reject stale observer/UI effects; scenario tests for shared owners, cancellation/re-request race, priority/concurrency, hidden reveal; grouped mutants; focused/full60s-file/lint/build/browser; commit/merge/normal landq/ancestry then close and report.
|
||||
@@ -0,0 +1,3 @@
|
||||
# client-reliability-4
|
||||
|
||||
Investigation only at4c54858c. URL queued media last-owner disposal reproduced: obsolete starts1 expected0; shared live-owner control passes. See test_logs/TEST_LOG_2026-10-20_03-10-00_client-reliability-4.md and /tmp/bitvid-client-reliability-4-url-lifecycle.mjs. Await dispatch scope before product/tests.
|
||||
@@ -0,0 +1,3 @@
|
||||
# client-reliability-4 complete
|
||||
|
||||
Product landed89ccb907, normal gate exit0 and7055496b ancestry verified. Backlog closed. All required local checks pass; evidence test_logs/TEST_LOG_2026-10-20_03-29-00_client-reliability-4-landed.md. Finish normal closure metadata landing/final event, then idle. No active abort, fallback/live peers,CPU or whole Stage3 claim.
|
||||
@@ -0,0 +1,3 @@
|
||||
# client-reliability-5
|
||||
|
||||
Investigation at0c1dd7e8:bounded active URL success/timeout cleanly settle; disposed active primary media error starts1 new mirror HEAD(expected0). Shared/rejoin and later same-source controls pass. Evidence test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md; scratch /tmp/bitvid-client-reliability-5-active-url.mjs. Await dispatch routing; no product/regression changes.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Item5 implementation
|
||||
|
||||
Controller-only6-line pre-candidate ownership guard. Six new lifecycle scenarios reproduce2 baseline failures and pass18/18 fixed;6 reverted mutants rejected. Unique integrity note test_logs/TEST_INTEGRITY_client-reliability-5.md. Focused/lint/build running; awaiting explicit dispatch full-suite/browser slots and verified unstable merge before final checks/landing. No active abort/observer/cache/limit changes.
|
||||
@@ -0,0 +1,6 @@
|
||||
# storage-cors-1
|
||||
- Start UTC: 2026-10-20T13:38:00Z; launch reported by dispatch ~13:37:59Z; acknowledgement 13:38:24Z.
|
||||
- Goal: validate current-HEAD existing-origin loss via real helper and offline SDK/transport; propose preservation semantics before edits.
|
||||
- Scope: investigation/evidence first; product/test changes require dispatch routing. No browser/cloud credentials/network bucket mutations; preserve preview3000.
|
||||
- Done: claimed; traced callers; 5 characterization cases reproduced. See docs/testing/storage-cors-integrity.md.
|
||||
- Pending: dispatch preservation semantics/fix routing, regression/mutants/focused checks, coordinated normal gates/build/landing.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Item5 complete
|
||||
|
||||
Product landed0bd0b1c3 via4bc52f9d,normal gate exit0/unstable ancestry verified. Backlog closed;expensive slot and browser released to dispatch13:57UTC. Local427files/28focused/18lifecycle/6mutants/lint11/build/native browser smoke pass. Finish docs-only closure queue/final event then idle. Evidence test_logs/TEST_LOG_2026-10-20_13-58-00_client-reliability-5-landed.md. No active abort/playback/CPU claim.
|
||||
@@ -0,0 +1,6 @@
|
||||
# storage-cors-2
|
||||
- Current route: read-only remaining storage/delete-safety audit per dispatch14:58:59; former feed scope deferredstorage3.
|
||||
- Actual real-service malformed cached bucket/base reproduction2rejects vs0required;6controls pass,existing focused11scenarios/3files pass. docs/testing/storage-cors-delete-audit.md has fidelity/callers/limits.
|
||||
- Product/test read-only until routing. Proposed defensive primitive-string handling/structured skip in r2Service.deleteVideoStorage + focused new regression. s3UploadService contains no duplicate delete method; no upload/key changes proposed.
|
||||
- Pending: dispatch fix routing;claim item2 after queued metadata visibility. QA owns full/normal/browser;no full/build/browser/cloud.
|
||||
- Scratch /tmp/storage-cors-delete-audit.mjs;deferred feed invalid-seed scratch /tmp/storage-cors-feed-audit.mjs preserved. No secret/private-data use.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Item6 audit
|
||||
|
||||
Atd88716e3 real profile remove local/synced controls pass;historical runtime no-op fails both actual-wiring diagnostics after cleanup but existing profile27 stillpass. Valuable missing regression,notcurrentproductfailure. Verified metadata merge tobbf57388/claimed;profile product/tests unchanged. Await test-only routing;notes only,no product/test commits. See test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md. QA full/normal slot respected.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Item6 retention
|
||||
|
||||
New tests/profile-history-removal-wiring.test.mjs retains3 actual profile/factory/DOM/controller/service scenarios;five runtime mutants fail with0cancelled. Product and existing suites unchanged. Focused/lint running;no full/browser/build until explicit resource grant. Unique integrity in test_logs/TEST_INTEGRITY_client-reliability-6.md. No real signer/relay/fullapp/reload claim.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Item7 cold live feed notification
|
||||
|
||||
- Goal: cold cache then eligible live updates must notify actual loadVideos consumer;latest feed owns reused subscription and clear invalidates late callbacks.
|
||||
- Scope: nostrService loadVideos notification state plus constructor/clear lifecycle only;new focused service test and unique evidence/backlog. No app/feed/filter/cache policy/runner changes.
|
||||
- Plan: reproduce original service with controlled subscribeVideos boundary;prove latest/force/clear controls;minimal current-consumer token;mutants/focused/lint;explicit full slot then one unchanged normal gate;QA owns actual-app browser after landing/preview rebuild.
|
||||
- Original4e904349: new first5 scenarios4fail1pass/0cancel;first8line callback fix later5pass2fail on7. Revised service19 added lines passes7/7. No actual browser/full/build yet.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Local review scope
|
||||
|
||||
- Owner milestone:Ready for me to review and test locally.
|
||||
- Codefixb1174163/full432gate0;preview6da89895/HTTP200/indexcmp0.
|
||||
- Actualcoldfeed+separate25step3viewportcoverage complete;originalUpload2/4pxoverflowfailureopen,notwholeallpass.
|
||||
- Finish durable named QA/dispatch docs and final runtime/preview audit;thenIDLE,nooptionalnewwork.
|
||||
- Live and deferred gates:docs/agents/LOCAL-REVIEW.md andfleet/QUALITY.md.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Goal: measure active URL-media cancellation at integrated HEAD5b77fd20 before proposing minimal fix.
|
||||
- Scope: real observer/controller/cache with controlled external HEAD/media IO; scratch/evidence only.
|
||||
- Done: resource/listener/timer/promise/slot/FIFO/cache/inflight/stale-write measurements; shared/rejoin/cross-controller/legacy controls; exact reproduction and scoped proposal to dispatch.
|
||||
- No product/committed-test changes or full/build/browser without routing/grant. Preserve prior queued-only assertions pending explicit contract correction.
|
||||
@@ -0,0 +1 @@
|
||||
Six-priority campaign resumed via interpreter; see docs/agents/CAMPAIGN-2026-10-22.md. Scope administrative coordination and evidence-first routing only. No product edits. Definition of done covers all six and final integrated local review with explicit live/manual limits.
|
||||
@@ -0,0 +1 @@
|
||||
Client9 at c5b678f7; investigate HEAD owner cancellation only via actual observer/controller and controlled fetch. No product/retained tests/full/build/browser. HLS/torrent remain separate. Success: counts for disposal/shared/rejoin/cache/timers/media and scoped proposal to dispatch.
|
||||
@@ -0,0 +1,4 @@
|
||||
- storage-cors-3 resumed by dispatch/campaign; baseline c5b678f7. Read-only product/retained tests, offline synthetic data only.
|
||||
- Goal: real deletion ingest followed by stale refresh/late older/view switch; preserve unrelated/newer content under existing semantics.
|
||||
- Seven scratch scenarios: three controls pass, four strict failures. Evidence docs/testing/storage-cors-3-investigation-2026-10-22.md; script /tmp/bitvid-storage-cors-3-audit.mjs.
|
||||
- Await independent reproduction/precise ownership routing. No browser/full/build/normal submission or live services authorized for this investigation.
|
||||
@@ -0,0 +1 @@
|
||||
Client9 HEAD-only candidate ready: controller cancel hook/abort+raced prompt settlement/timer identity cleanup;23new HEAD+37existing scenarios pass,16failing isolated mutants/cancelled0,lint11 pass. No existing test expectation edits. Explicit strict contract/integrity notes. Next commit candidate, merge current unstable, request full/browser slot; do not submit or run expensive jobs without grant. HLS/torrent separately open.
|
||||
@@ -0,0 +1,5 @@
|
||||
- storage-cors-4: prevent obsolete registered feed results repainting deleted/replaced DOM.
|
||||
- Baseline26e9a787; storage3 durable63e2b519 normalgateexit0, ancestry verified; close3 after durable lookup.
|
||||
- Own coordinator only plus unique tests/notes. No app/view/engine/service/buffer edits.
|
||||
- Plan: retain actual registered pipeline/JSDOM baseline; guard refresh/load ownership; cover async runtime/engine success+error, disposal/remount, telemetry and current controls; kill restored mutants; focused/lint; request serialized normal gate; verify durable landing.
|
||||
- Definition of done: strict regressions+mutants, existing focused/lint, merged normal gate, durable status+ancestry. No live/browser qualification inferred.
|
||||
@@ -0,0 +1 @@
|
||||
Client9 HEAD-only fix durably landed f41ac656 after normal435-filegate exit0, ancestry0; sequential build/artifact exit0. Heavy slot released. Await separate bounded native browser slot; no browser started. HLS/torrent/RangeGET and wholeStage3 remain open. No additional product changes.
|
||||
@@ -0,0 +1 @@
|
||||
HEAD slice complete locally: durablef41ac656 normal435unique/exit0/ancestry0; build/artifact0; controlled native Chromium+Firefox lifecycle pass with synthetic media completion/public-deny. Browser/heavy resources released. Pending docs-only evidence landing; HLS/torrent/RangeGET remain open, await bounded routing/HEAD closure disposition.
|
||||
@@ -0,0 +1 @@
|
||||
Item9 HEAD slice done/landedf41ac656. Read-only HLS diagnostic atd04d501e actualobserver/controller/helper reproduces5cancellationfailures/7controls pass/0cancelled/naturalexit1. Pending manifest GET/body retains timer and promise after final-owner disposal; rejoin original,late cache. Need dispatch explicit controller+HLShelper ownership/contract before edits. No browser/full/build/product/retainedtest changes.
|
||||
@@ -0,0 +1 @@
|
||||
HLS-only implementation perdispatch20:55:13Z/confirmed newestRULE. Controller passes optional lifecycle; manifesthelper cancellation covers fetch+body, synchronous invalidation/promptcancelled/abort/identitysafehook+timercleanup. No engine/player/session/HEAD/media/torrent/RangeGET changes. New35tests+adjacent94=129pass/6files;19isolatedmutantsfailnaturally/cancelled0. Final lint pending; record then commit+merge currentunstable,READY for serialized full/browser/build/normal queue. No expensive stage/submission without grant.
|
||||
@@ -0,0 +1 @@
|
||||
Client9 resumed at5f8e58be, existing item reclaimed. Runtime identical unstable and prior146076dd handoff;4 browser evidence docs retained. Current narrow goal: diagnose original Firefox HLS body-entry prerequisite using preserved unexecuted d0100521 instrumented scratch, only after fresh dispatch slot+cooling clearance. No product/test edits or broader cancellation work. Preserve all failures and strict5000ms bound; no repeat full/build needed for unchanged runtime. See matching test log; dispatch blocker sent.
|
||||
@@ -0,0 +1,4 @@
|
||||
Goal: reconcile existing HLS evidence and recommend remaining lifecycle boundary under interpreter13:41 routing.
|
||||
Success: preserve prior failure/control results, verify current source ancestry/equality and raw evidence hash, commit existing dirty notes, send scoped recommendation.
|
||||
Non-goals: no product/tests/browser/full/build/submission, no live or wholeStage3 claim; QA owns sole browser.
|
||||
See matching reconciled test log. Next: await explicit offline active-torrent investigation routing.
|
||||
@@ -0,0 +1,4 @@
|
||||
Goal: interpreter-authorized current-HEAD offline active torrent lifecycle investigation, no implementation.
|
||||
Baseline HEAD e9af76df, real observer/grid/queue/probePeers, fake WebTorrent IO+clock.
|
||||
Result: corrected12cases7pass5fail/naturalexit1; cleanup0remaining resources; existing lifecycle11/11 naturalexit0. See matching TEST_LOG.
|
||||
Next: dispatch independent reproduction and explicit gridHealth/probePeers-only ownership plus spec correction before fixes/tests. QA owns browser; no full/build/submission.
|
||||
@@ -0,0 +1 @@
|
||||
Approved dispatch routing read locally: actual baseline independently reproduced12=7pass5fail, no repeat needed. Scope gridHealth+optional probePeers lifecycle only. Goal final-owner null settlement/resource cleanup/fresh identity; preserve shared/standalone/live/cap4/FIFO/TTL/deferred destroy safety. Spec and integrity written first. Focused offline/mutants/targeted lint allowed; no full/build/browser/submission until resource grant.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client10 goal and scope
|
||||
|
||||
- Authority exact dispatch ROUTING-2026-10-27-client10-animation-cleanup.md read; sourcef01ce012 after safe ff, untracked client9 note preserved.
|
||||
- Goal recover already-finished transient interface fade/delay classes during delayed cleanup init; preserve active/future interface animation and thumbnail end/cancel semantics. Existing listener-only cleanup cannot observe an event already dispatched.
|
||||
- Done now: minimal index block extraction + helper, scenario/wiring regressions, killing mutants for all cases, lint under freshhealth. Fullgate/landing/build/nativebrowser are later explicit slots. No other bootstrap/CSS/app edits or oldoverflow closure.
|
||||
- Plan: create/claim10; record contract/testintegrity; extract existing cleanup; reconcile finished own-target CSS interface animations with no stillactive counterpart; test real helper and extracted bootstrap wiring; isolated mutants; lint; namedpathcommit/merge and READY only, no submit.
|
||||
- getAnimations/finished/fill reference https://www.w3.org/TR/web-animations-1/; finished forwards effects remain relevant and can retain stacking context. Avoid sleeps, timers, layout reads, arbitrary phase-duration guesses and broad class stripping.
|
||||
+306
-36
@@ -751,6 +751,68 @@
|
||||
transition-property: border-color, box-shadow, transform;
|
||||
}
|
||||
|
||||
.site-update-notice {
|
||||
@apply fixed bottom-5 right-5 z-50 flex max-w-sm flex-col items-end gap-3;
|
||||
}
|
||||
|
||||
.site-update-notice__button {
|
||||
@apply btn focus-ring rounded-full px-4 py-3 text-sm shadow-lg;
|
||||
}
|
||||
|
||||
/* Fixed mobile actions need to clear the browser home indicator as well as
|
||||
the visual spacing requested by their component. */
|
||||
@media (max-width: calc(theme("screens.md") - 0.02px)) {
|
||||
.mobile-safe-bottom {
|
||||
bottom: calc(var(--space-lg) + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
.site-update-notice__panel {
|
||||
@apply w-full rounded-2xl border border-border bg-overlay-panel-soft p-4 text-left text-text shadow-modal backdrop-blur-sm;
|
||||
}
|
||||
|
||||
.site-update-notice__close {
|
||||
@apply btn-ghost focus-ring -mr-2 -mt-2 h-9 w-9 rounded-full p-0 text-lg leading-none;
|
||||
}
|
||||
|
||||
.site-tagline__text {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media (max-width: calc(theme("screens.md") - 0.02px)) {
|
||||
/* Both controls are fixed to the lower-right corner on phones. Keep the
|
||||
site-update notice above the search FAB so neither tap target obscures
|
||||
the other. */
|
||||
.site-update-notice {
|
||||
right: var(--space-lg);
|
||||
bottom: calc(
|
||||
var(--space-lg) + var(--space-2xl-plus) + var(--space-sm)
|
||||
);
|
||||
max-inline-size: min(
|
||||
var(--size-20rem),
|
||||
calc(100vw - (var(--space-lg) * 2))
|
||||
);
|
||||
}
|
||||
|
||||
.site-update-notice__button {
|
||||
min-block-size: var(--size-11);
|
||||
padding-inline: var(--space-md);
|
||||
padding-block: var(--space-sm);
|
||||
}
|
||||
|
||||
.site-tagline {
|
||||
margin-block: var(--space-lg) var(--space-xl);
|
||||
padding-inline: var(--space-md);
|
||||
}
|
||||
|
||||
.site-tagline .site-tagline__text {
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: var(--line-height-tight);
|
||||
letter-spacing: var(--tracking-normal);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.login-option::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -880,6 +942,29 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Maps the <bitlogin-auth> widget's own theming hooks onto bitvid's design
|
||||
tokens (components/login-modal.html), so it reads as part of this site
|
||||
rather than a foreign embed, and follows the light/dark theme toggle
|
||||
automatically since every token below is itself theme-aware. */
|
||||
bitlogin-auth {
|
||||
display: block;
|
||||
--bl-accent: var(--color-accent-strong);
|
||||
--bl-accent-hover: var(--color-accent);
|
||||
--bl-accent-fg: rgb(255 255 255);
|
||||
--bl-bg: var(--surface-panel);
|
||||
--bl-fg: var(--text-primary);
|
||||
--bl-muted: var(--text-muted);
|
||||
--bl-border: var(--border-default);
|
||||
--bl-input-bg: var(--surface-sunken);
|
||||
--bl-danger: var(--status-danger);
|
||||
--bl-danger-bg: var(--status-danger-surface);
|
||||
--bl-warn: var(--status-warning);
|
||||
--bl-warn-bg: var(--status-warning-surface);
|
||||
--bl-radius: var(--radius-lg);
|
||||
--bl-font-family: inherit;
|
||||
--bl-max-width: none;
|
||||
}
|
||||
|
||||
.btn-ghost[data-state="active"],
|
||||
.btn-ghost[aria-pressed="true"] {
|
||||
@apply border-transparent bg-primary text-white shadow-md;
|
||||
@@ -944,7 +1029,7 @@
|
||||
}
|
||||
|
||||
.moderation-badge {
|
||||
@apply inline-flex flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm;
|
||||
@apply inline-flex max-w-full flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm;
|
||||
}
|
||||
|
||||
.moderation-badge--interactive {
|
||||
@@ -952,11 +1037,11 @@
|
||||
}
|
||||
|
||||
.moderation-badge__actions {
|
||||
@apply inline-flex flex-nowrap items-center gap-sm;
|
||||
@apply flex max-w-full flex-wrap items-center gap-sm;
|
||||
}
|
||||
|
||||
.moderation-badge__label {
|
||||
@apply inline-flex items-center gap-xs whitespace-nowrap;
|
||||
@apply inline-flex min-w-0 items-center gap-xs;
|
||||
}
|
||||
|
||||
.moderation-badge[data-variant="warning"] {
|
||||
@@ -996,11 +1081,12 @@
|
||||
}
|
||||
|
||||
.moderation-badge__text {
|
||||
@apply whitespace-nowrap text-xs font-semibold tracking-tight;
|
||||
@apply min-w-0 text-xs font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
.moderation-badge__action {
|
||||
@apply inline-flex items-center gap-1.5 rounded-full border border-status-warning-border bg-status-warning-surface px-3 py-1 text-xs font-semibold normal-case tracking-tight text-status-warning-on transition duration-150 ease-out;
|
||||
@apply inline-flex max-w-full items-center gap-1.5 rounded-full border border-status-warning-border bg-status-warning-surface px-3 py-1 text-xs font-semibold normal-case tracking-tight text-status-warning-on transition duration-150 ease-out;
|
||||
overflow-wrap: anywhere;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--status-warning) 16%,
|
||||
@@ -1487,6 +1573,9 @@
|
||||
.modal-sheet {
|
||||
inline-size: 100vw;
|
||||
block-size: 100vh;
|
||||
block-size: 100dvh;
|
||||
min-block-size: 100vh;
|
||||
min-block-size: 100dvh;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
@@ -3553,6 +3642,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.profile-modal__menu-heading,
|
||||
.profile-modal__mobile-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-modal__pane-wrapper {
|
||||
@@ -3733,55 +3828,185 @@
|
||||
Chromium drops the whole block. `theme("screens.lg")` resolves to a literal at
|
||||
build time, which is valid. */
|
||||
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
|
||||
.profile-modal__identity {
|
||||
align-self: stretch;
|
||||
max-inline-size: none;
|
||||
min-block-size: var(--size-6rem);
|
||||
margin-inline: var(--profile-modal-padding-inline);
|
||||
margin-block: 0;
|
||||
padding: var(--space-md);
|
||||
border: 0;
|
||||
border-radius: var(--radius-xl);
|
||||
color: var(--color-white);
|
||||
background:
|
||||
radial-gradient(
|
||||
100% 130% at 100% 0%,
|
||||
color-mix(in srgb, var(--color-accent) 82%, transparent),
|
||||
transparent 58%
|
||||
),
|
||||
linear-gradient(
|
||||
120deg,
|
||||
var(--color-text-strong) 0%,
|
||||
color-mix(in srgb, var(--color-text-strong) 82%, var(--color-accent)) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.profile-modal__identity-avatar {
|
||||
inline-size: var(--space-2xl-plus);
|
||||
block-size: var(--space-2xl-plus);
|
||||
border-color: var(--color-white);
|
||||
}
|
||||
|
||||
.profile-modal__identity-kicker {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.profile-modal__identity-name {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.profile-modal__menu {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-block-end: max(var(--space-lg), env(safe-area-inset-bottom));
|
||||
scroll-padding-block-end: max(
|
||||
var(--space-lg),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.profile-modal__menu-heading {
|
||||
display: block;
|
||||
padding-block: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
.profile-modal__menu-title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.profile-modal__menu-hint {
|
||||
margin: var(--space-3xs) 0 0;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.profile-modal__nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
/* Every tile the SAME height, so a 2-line label can't stretch its row
|
||||
taller than the others (the "ragged grid of ellipses" fix). */
|
||||
grid-auto-rows: var(--profile-modal-nav-tile-height);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.profile-modal__nav > button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: var(--space-2xl-plus) minmax(0, 1fr) var(--space-lg);
|
||||
grid-template-rows: auto auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: var(--space-2xs);
|
||||
justify-content: start;
|
||||
column-gap: var(--space-md);
|
||||
row-gap: var(--space-3xs);
|
||||
width: 100%;
|
||||
block-size: 100%;
|
||||
/* Consistent rounded-square tiles, not rounded-full ellipses (btn's
|
||||
default) that turn into circles once a tile gets taller. */
|
||||
border-radius: var(--radius-xl);
|
||||
/* Subtle fill so they read as solid cards, not floating outlines. The
|
||||
active tile's bg-primary (higher specificity) still wins. */
|
||||
min-block-size: var(--size-5-75rem);
|
||||
text-align: start;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
line-height: var(--line-height-tight);
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: var(--color-border-subtle);
|
||||
background-color: var(--color-panel);
|
||||
padding-block: var(--space-xs);
|
||||
padding-inline: var(--space-3xs);
|
||||
line-height: 1.15;
|
||||
padding: var(--space-sm);
|
||||
overflow: hidden;
|
||||
/* wrap labels at spaces, not mid-word */
|
||||
overflow-wrap: break-word;
|
||||
word-break: normal;
|
||||
hyphens: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.profile-modal__nav > button[data-state="active"] {
|
||||
color: var(--color-text);
|
||||
border-color: color-mix(in srgb, var(--color-accent) 36%, transparent);
|
||||
background-color: color-mix(in srgb, var(--color-accent) 9%, var(--color-panel));
|
||||
box-shadow:
|
||||
inset var(--size-3px) 0 0 var(--color-accent),
|
||||
var(--shadow-md);
|
||||
}
|
||||
|
||||
.profile-modal__nav > button:focus-visible {
|
||||
outline-offset: calc(var(--space-3xs) * -1);
|
||||
}
|
||||
|
||||
/* slightly larger glyph for the tile */
|
||||
.profile-modal__nav > button svg {
|
||||
width: var(--space-lg);
|
||||
height: var(--space-lg);
|
||||
flex-shrink: 0;
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 2;
|
||||
width: var(--space-xl);
|
||||
height: var(--space-xl);
|
||||
padding: var(--space-sm);
|
||||
box-sizing: content-box;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-page-alt);
|
||||
}
|
||||
|
||||
.profile-modal__nav > button::before {
|
||||
content: "";
|
||||
grid-column: 3;
|
||||
grid-row: 1 / span 2;
|
||||
inline-size: var(--space-xs);
|
||||
block-size: var(--space-xs);
|
||||
border-block-start: var(--border-width-hairline) solid currentColor;
|
||||
border-inline-end: var(--border-width-hairline) solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.profile-modal__nav > button::after {
|
||||
content: attr(data-mobile-description);
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
align-self: start;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 400;
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
|
||||
.profile-modal__nav > button[data-state="active"] svg {
|
||||
color: var(--color-white);
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* In the tile grid the unread dot rides the tile's top-right corner instead
|
||||
of the vertical-nav left edge (where it landed in the grid gutter). */
|
||||
.profile-modal__nav > button .profile-nav__unread-dot {
|
||||
top: var(--space-2xs);
|
||||
top: var(--space-sm);
|
||||
left: auto;
|
||||
right: var(--space-2xs);
|
||||
right: calc(var(--space-sm) + var(--space-lg));
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.profile-modal__mobile-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: var(--space-xs);
|
||||
padding-block-start: var(--space-xs);
|
||||
padding-block-end: max(
|
||||
var(--space-sm),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in srgb, var(--color-page) 0%, transparent),
|
||||
var(--color-page) var(--space-sm)
|
||||
);
|
||||
}
|
||||
|
||||
.profile-modal__mobile-actions > button {
|
||||
flex: 1 1 0;
|
||||
justify-content: center;
|
||||
min-block-size: var(--size-11);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: theme("screens.lg")) {
|
||||
@@ -4491,6 +4716,39 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* The player owns a single scroll region. On mobile, `100vh` can extend
|
||||
underneath the browser controls/home indicator, which previously made the
|
||||
final composer action hard to reach. Prefer the dynamic viewport and keep
|
||||
scrolling on the panel (where the sticky player navigation listens). */
|
||||
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
|
||||
#playerModal {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#playerModal > .bv-modal__panel {
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
#playerModal .player-modal__container {
|
||||
block-size: auto;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
|
||||
#playerModal .comment-composer {
|
||||
padding-bottom: max(var(--space-lg), env(safe-area-inset-bottom));
|
||||
scroll-margin-block-end: max(var(--space-lg), env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
#modalVideo {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
@@ -6662,6 +6920,18 @@
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
|
||||
#profileModal {
|
||||
height: 100dvh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
#profileModal .bv-modal__panel {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#profileModal .bv-modal__panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Use the existing backlog as the starting point; verify stale reports. Keep the initial team as small as evidence permits. No product edits by dispatch. Conservative operating limits are manager defaults, not attributed owner quotes.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Approved worker bootstrap
|
||||
|
||||
- Accept scoped routine-work relay with explicit provenance; spend/publish/deploy approvals remain session-local and unauthorized.
|
||||
- Staff one client-reliability worker, no subagents. Preserve preview and schedules.
|
||||
- One-time administrative-only queue gate verifies exact expected roster and doc/backlog diff; no persistent gate modification or product/test change. This resolves roster-before-launch versus existing suite-hang dependency under the owner's routine local landing authority. Report the unresolved test baseline.
|
||||
@@ -0,0 +1,9 @@
|
||||
# client-reliability-1: classify the profile-modal timeout
|
||||
|
||||
- Product/test source unchanged between filedAt 8b204687 and startup HEAD 9e6c310d.
|
||||
- Clean-environment 60s/file run reproduced timeout. Per-scenario tracing then completed all 26 scenarios naturally in 57041.55ms; no blocked await or retained resource was found. Timing near the bound varies with cold/warm JSDOM style caches.
|
||||
- CPU profile of the Escape scenario points to CSS color/border normalization and computed-style resolution. The full markup has many controls; updateFocusTrap asks JSDOM for a full computed declaration for each control on repeated pane refreshes.
|
||||
- Fix only the stylesheet-free test fixture: virtualize display/visibility, with explicit fixture CSS and a new native-JSDOM contract scenario. Preserve every existing assertion and real controller, focus events, DOM and pane operations. No timeout/retry/sleep/runner or product change.
|
||||
- Regression compares fixture/native style filtering for visible, disabled, CSS-hidden, inherited-visibility-hidden, aria-hidden, and hidden-type controls, then checks actual Tab event focus wrapping.
|
||||
- Limits: fixture does not model application CSS/layout. This suite never loaded those styles; browser and build checks are not required for this test-only fix. No release, browser, signer, network or wallet qualification claimed.
|
||||
- Slowdown: initial cold worktree lacked dependencies; cached offline install resolved it. Native DOM assertion diffs made the first mutant diagnostic expensive; comparing control IDs yields a bounded useful failure. Tool improvement: show per-scenario start/end timestamps when a file exceeds its diagnostic bound, distinguishing slow progress from a blocked await.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Bounded follow-up routing
|
||||
|
||||
- Retain one worker: this is another test-harness boundary; no evidence warrants another product-code owner.
|
||||
- Grant one race-test file plus regression/integrity notes. Product/shared-helper scope needs explicit routing.
|
||||
- Preserve separate commits, assertions, natural process exit, and normal full landing gates. No test fix landed yet.
|
||||
@@ -0,0 +1,8 @@
|
||||
# client-reliability-2: hermetic concurrent init
|
||||
|
||||
- NIP01 REQ/CLOSE and docs/architecture-refactor.md L1 contract require callers to close subscriptions. Retain real init/L1/feed behavior; fake only IO boundaries.
|
||||
- Root cause: original client.connectToRelays stub returned [], explicitly signaling initial connection failure. init schedules ConnectionManager's timer, whose callback calls manager.connectToRelays (not the client stub). That path attempts configured public relays and retains retries. Original subscription was also never unsubscribed.
|
||||
- Replace 50ms mock sleep with deferred entry/release barriers. Successful manager-level connection fixture prevents retries without disabling product retry logic. Pin timer absence with assertion; teardown resets as failure-path hygiene, not a success substitute.
|
||||
- Add stricter outcomes: pending second caller, observed pool feed filter, nonempty relays, and exactly-once transport closure. Preserve all original expectations and await both callers.
|
||||
- Mutate via test-local IO/init overrides only; product Nostr/shared helpers/global runner remain untouched. Both early-return and failure-connection variants fail naturally.
|
||||
- Prior profile-modal commits preserved separately. No actual signer/wallet/relay/browser/build/deploy qualification. Normal full gates required before either item lands.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Next bounded task and coordination qualification
|
||||
|
||||
- Keep one worker; prior test tasks done, product grid-health scope is now isolated.
|
||||
- Require actual observer-to-probe lifecycle reproduction, not source-extracted queue inspection. Dispatch sees1 obsolete start after grid disposal, expected0.
|
||||
- Bound fix to queued work. Shared live owners and later requests must survive; cancelled work must not populate negative cache.
|
||||
- Mailbox files are delivery/storage, not wakeup. Record missing automatic dispatch continuation and require explicit completion evidence; no unattended qualification, scheduler or kit change.
|
||||
@@ -0,0 +1,9 @@
|
||||
# client-reliability-3: queued probe ownership
|
||||
|
||||
- Actual baseline at66832dc3 (source unchanged from filedAtf9adfc8f): actual attachHealthBadges + DOM + observer, four deferred probePeers calls active and fifth queued; disposing/removing feed then releasing active work starts one obsolete probe (expected0), assertion exit1.
|
||||
- Queue records own a Set of live card requests. onCardUnregister releases the matching request; only an unstarted job with no remaining owner can cancel. Cancellation resolves null, skips normalization/cache, and removes inflight synchronously. Promise finalization checks record identity so an old cancellation cannot delete replacement inflight work.
|
||||
- Active jobs are never aborted or removed from inflight on disposal; their normal cache result remains usable. Existing raw magnets/webseeds, four-slot concurrency, stable priority ordering, healthy/negative TTL remain unchanged.
|
||||
- Live-card membership rejects late observer entries. Each request also guards result writes by ownership/identity even while disposed DOM remains connected. Per-registration badge listener removal and idempotent disposer prevent stale callbacks or an old disposer unregistering a newly attached feed.
|
||||
- Existing cardObserver unregister hook is sufficient; no shared helper, URL health, WebTorrent implementation, app, cache policy, concurrency, styling, runner or kit edits.
|
||||
- Browser smoke uses actual built modules/native observers and fake external probe IO only; both Chromium and Firefox pass. No external request attempted. Hosted-only/torrent-only source visibility checked; real playback fallback, live torrent peers, CPU/RSS and active abort behavior unmeasured/out of scope.
|
||||
- Fixture correction before acceptance: use explicit viewport geometry so observer priorities are deterministic; initial unpinned JSDOM viewport reversed two fixture assumptions. No existing assertion changed. Eleven mutants independently kill the nine grouped lifecycle scenarios (details in integrity note).
|
||||
@@ -0,0 +1,7 @@
|
||||
# Final callback ownership corrections
|
||||
|
||||
- Caller review shows app.boundStreamHealthBadgeHandler is shared across feeds; preserve global callback dedup with registration reference counts. Disposing one owner must retain the handler for another owner; final disposal removes it. Wrapping each registration separately would duplicate callbacks.
|
||||
- Synchronous checking notifications can dispose/rerender the feed before queue submission. Measured1 start after disposal, expected0; check live membership after notification before creating queued work.
|
||||
- Late observer scenario also asserts no disposed-card notifications reach a remaining registration, not just final badge state. This rejects a removed observer guard even when the reentrant guard still prevents queue submission.
|
||||
- Final source has11 lifecycle scenarios;14 independent mutants were rerun against this exact product tree, all exit1 with1 failed/0 cancelled, then restored. Source outside gridHealth.js remains untouched.
|
||||
- Corrected build and Chromium/Firefox built-dist/native-observer smoke both pass; no external requests or page errors. Actual media playback fallback and CPU/RSS remain unverified.
|
||||
@@ -0,0 +1,3 @@
|
||||
# client-reliability-4
|
||||
|
||||
Stop at smallest reproduced queued URL media defect. Keep active URL/torrent cancellation separate. Exercise real cache/observer/controller and only virtualize external IO. Await explicit routing before product/regression tests.
|
||||
@@ -0,0 +1,7 @@
|
||||
# client-reliability-4 implementation
|
||||
|
||||
- Existing observer unregister boundary owns per-badge lifecycle tokens, read by controller via WeakMap. Existing app/feed callback arguments unchanged; direct legacy controller consumers remain live until settlement.
|
||||
- Module-scoped promise→ownership WeakMap mirrors shared real inflight cache across controllers. Last-owner release only cancels queued media jobs. Already-running media/cache behavior remains; no active HEAD/media/HLS/torrent abort.
|
||||
- Queue resumes reserve a slot synchronously to preserve three-media cap. Cancellation settles with an explicit internal outcome propagated through mirrors and omitted from health cache. Existing state-cache promise identity guard preserves immediate re-request.
|
||||
- Guards bind UI/RAF work to captured registration; late observer callbacks/eager microtasks and stale disposer cannot affect reused registrations.
|
||||
- No app/shared observer/cache-module or policy/TTL edits. Scope/read-only predecessor evidence at77d486fb; dispatch implementation routing merged before code.
|
||||
@@ -0,0 +1,3 @@
|
||||
# client-reliability-5
|
||||
|
||||
Do not equate retained active work with a leak: explicit success and controlled real timeout release resources/slots and cache correctly. Route only demonstrated new mirror HEAD after disposal; guard candidate continuation in controller before considering active abort or broader resource policy. Existing observer token is sufficient.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Item5 continuation boundary
|
||||
|
||||
- Keep bounded already-active success/failure caching. Check existing lifecycle owners only BEFORE starting each candidate in probeUrlList; zero owners marks existing record cancelled and returns explicit cancellation. No probe abort or observer/cache/TTL/concurrency change.
|
||||
- Shared and rejoining owners remain eligible because record owners are checked at continuation time. Completed final-source failure keeps original cache behavior; cancelled mirror chains never cache offline.
|
||||
- Six focused regressions exercise actual observer/controller/cache plus external IO twins; unique integrity file during parallel trial.
|
||||
- Dispatch resource coordination: local full suite is next after administrative drain but requires explicit grant; browser belongs initially to playback QA. All CLI/gate launches clear inherited NODE_TEST_CONTEXT operationally; no kit/runner change.
|
||||
@@ -0,0 +1,6 @@
|
||||
# storage-cors-1 proposed semantics (awaiting dispatch)
|
||||
- Preserve every existing rule and field; do not union new origins into arbitrary rules (would grant their permissions to new origins).
|
||||
- Read existing policy before writing; only explicit NoSuchCORSConfiguration indicates absent policy. Other failures propagate without PUT.
|
||||
- Add the existing default rule for newly requested origins separately. Skip PUT when equivalent existing default rules already cover every requested origin.
|
||||
- Preserve wildcard rules verbatim; never infer extra methods/headers or edit unknown fields. Exact equivalence/coverage details require routing before implementation.
|
||||
- No dependencies/SDK version change. Inspect SDK export read-only until routing.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Item6
|
||||
|
||||
Classify historical no-op as a regression gap: actual wiring works,but existing fake-renderer profile suite passes27/27 even with historical override. Propose focused new real-wiring test and unique integrity,not product or app/history-service/cache edits. Exercise DOM event and backing persistence/recreation;fake only synced external IO and unrelated constructor dependencies. No live signer/relay or full app/reload claim.
|
||||
@@ -0,0 +1,6 @@
|
||||
# storage-cors-2
|
||||
- Reject malformed connection fields as invalid-connection; no String(object) or arbitrary trim invocation for cleanup authorization/base/key material.
|
||||
- Reject nonstring owner identifiers as invalid-owner before resolution. Null/non-object parameter options safely use existing no-videos result.
|
||||
- Preserve existing owned-base/key collector,credential lock reasons,per-object failure summaries,provider-aware client branches and content-addressed upload semantics.
|
||||
- s3UploadService contains no affected delete counterpart; no upload-copy edits per explicit dispatch ruling. Tests prove both provider branches instead.
|
||||
- No broad catch; existing client-construction and each-delete failures retain structured results. Arbitrary throwing getters/proxies beyond serializable cache metadata are outside this proof.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Item6 test design
|
||||
|
||||
- Use actual profile config/factory and DOM click with original renderer removal/controller/local service. Observe actual removal promise only for deterministic completion. Required unrelated constructor services and read/metadata boundary are controlled; no fake renderer/remove implementation.
|
||||
- Local and synced removal include final-item case and renderer recreation; local Clear is a separate scenario with real queue reset. No synced Clear assumption.
|
||||
- Synced IO twin respects replace vs merge, so missing replace is observable rather than rubber-stamped. Assert all published payload/options plus publication occurrence, without requiring duplicate implementation calls.
|
||||
- Isolated runtime mutants exercise historical config no-op,clear no-op,drop-replace,wrong actor and final-item no-op. Explicit teardown runs on assertion failure and restores queue/app/client/globals.
|
||||
- No product/existing suite edits; focused/lint only until dispatch allocates full/normal slot. Build/browser remain unverified/outside current grant for test-only retention.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Item7 notification ownership
|
||||
|
||||
- Live updates notify the same full ordered cached list emitted/persisted already,including deletion-only empty results. Preserve event payload/deleted metadata and filtering.
|
||||
- A service-owned notification object carries latest loadVideos consumer across reused subscription. Creating replacement subscription uses fresh identity;clear invalidates before unsub. Old subscription callback cannot change cache,notify consumer,emit or persist after disposal.
|
||||
- Interpreter independently identified stale captured consumer;our A→B and force/clear tests fail firstcallbackpatch,pass revisedtoken. Service lifecycle state explicitly routed by dispatch16:10:24.
|
||||
- Keep existing cache/persistent callbacks and callback-throw isolation. Event-only consumers remain supported. No productpolicy change/forced UI refresh. Browser qualification belongs to QA.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Local review decisions
|
||||
|
||||
- Fix demonstrated cold-feed hardblocker with actualservice regression/currentconsumer token;no forcedrefresh substitute.
|
||||
- One unchanged normal fullgate authoritative;do not duplicate whole suite solely to produce fullerlogs. Storedtail is not complete process transcript.
|
||||
- Preserve original strictUploadoverflowFAIL;separatefollowon captures remainingcoverage withoutallpassclaim. Minor2/4pxoverflow controlsusable/sourceunproven staysopen forlocalreview;no speculativeCSSfix/optionalpolishbatch. Interpreter independently agreesclassification.
|
||||
- Stop alloptionalwork at ownerlocalreviewmilestone;real signer/cloud/peer/production gates stayopen.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Implement only controller retainUrlProbe final-owner hook and active media settle/cleanup. Existing shared WeakMap owners and cancelled result paths suffice; no AbortController or observer/cache edit needed for video-element cleanup.
|
||||
- Mark cancelled synchronously before promise continuations so same-turn rejoin cannot attach to obsolete work. Clear active cancellation hook on completion; reuse existing finally for exactly-once slot/FIFO release.
|
||||
- Strict spec correction authorized at3dac0afb and recorded in docs/spec_changes/2026-10-20-client-reliability-8-active-media.md; retain live-owner cache/timeout/mirror contracts independently.
|
||||
- No full/build/browser before dispatch allocation; one unchanged normal full landing gate can be authoritative after merged focused/lint.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Reuse3 existing workers; disjoint investigations are useful, no extra agent justified yet.
|
||||
- Firefox gets browser/expensive slot first; cancellation and tombstone evidence use offline focused checks.
|
||||
- All6 priorities have durable accountable items; staged means unfinished, not cancelled.
|
||||
- Workers synchronize own clean branches; dispatch never alters peer worktrees.
|
||||
- No tests rerun for administrative docs; historical gates remain historical.
|
||||
@@ -0,0 +1,3 @@
|
||||
- HEAD slice only. Current media cancellation hook does not cover fetch; reproduce real ownership flow before proposing hook reuse.
|
||||
- Do not change product/tests without dispatch contract routing. Keep HLS/torrent and wide Stage3 completion open.
|
||||
- No redundant full/lint/build/browser for scratch investigation; QA slot untouched.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Treat kind30078 deleted markers using current timestamp semantics; do not infer new NIP09 or root/author ownership policy.
|
||||
- Use actual subscribe/buffer/client/service with fake transport and synthetic verifier; retain real filtered localStorage writes. Stub unrelated network hydration and IndexedDB scheduling only.
|
||||
- Capture actual coordinator render boundary with deferred engine decorator; explicitly leave full UI/DOM qualification open.
|
||||
- Route discovered shared service/buffer/coordinator changes before edits. No upload/cloud/CORS code changes.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Dispatch independent audit confirmed7=3pass/4fail and routed buffer/service only; merged routing076ec802 after evidence67140d89, merged HEAD89c5494a.
|
||||
- Buffer defers deletion of active versions to existing client.recordTombstone; preserve guarded-event same-ID cleanup. No new deletion timestamp/key policy.
|
||||
- Service rejects deleted/invalid/client-tombstone-covered objects before author/access bypass and direct caching; prune affected cached copies using authoritative guard plus existing explicit deletion IDs. Persist cleaned optimistic snapshots and empty active results.
|
||||
- Separate buffer commit ed5fb649; service commit follows. No app/feed coordinator/client.js writes. Coordinator race remains open pending faithful registered-pipeline/view/disposal evidence and new routing.
|
||||
@@ -0,0 +1,6 @@
|
||||
- Per-app WeakMap request and loader tokens avoid exposing a public API. Capture actual view, mounted container and telemetry context; only eligible completion may publish metadata/map/render/telemetry/impressions/similar content.
|
||||
- Capture mounted view.container (not mount adapter return); existing lightweight adapters need not expose a container. Real destroyed view loses its container and cannot be resurrected by completion.
|
||||
- Preserve started engine caller result: stale success resolves original videos/metadata; stale engine error resolves existing fallback/error metadata. loadFeedVideos still resolves undefined.
|
||||
- A request superseded while awaiting history preload never starts engine: resolve {videos: fallback, metadata: {reason, engine: "superseded"}}. This explicitly distinguishes a skipped run while preserving the resolved payload shape, and avoids new obsolete engine-stage subscriptions. No changes to current valid failure/unavailable fallback or content policies.
|
||||
- Old service callbacks and recommendation activation must also match the original load/view/context, including a same-feed replacement. Loader continuations recheck after each await before state writes.
|
||||
- No existing assertions changed. New failure injection wraps actual engine execution and rejects only after production stages, since moderation subscription rejection itself is deliberately caught by the production stage.
|
||||
@@ -0,0 +1,4 @@
|
||||
- Explicit dispatch20:55:13Z Stage3 HLS scope extension and helper/controller ownership; keep broaderitem9OPEN. Existing HEAD/media/engine/player/nativepreference behavior and standalone manifest callers preserved.
|
||||
- Pass optional lifecycle as third helper argument; helper owns its own hook/controller/timer through fetch AND response.text, races logical cancellation independently of AbortController cooperation. Native media remains on existing path.
|
||||
- Test actual observer/controller/cache/helper with controlled IO/clock boundaries, new unique suite only; existing tests/expectations unchanged. Baseline35newcases13pass/22fail before fix (isolated copies); original12mode5fail/7pass retained.
|
||||
- Mutants isolate copies in/tmp,never mutate worktree/shared modules. Initial retainedtimer mutant hit engineclearTimeout outside manifesthelper;corrected exacthelper suffix and final19allfail naturally. Formatting outside manifesthelper restored to keep engine read-only.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Preserve seven existing dirty notes verbatim alongside a new reconciliation, rather than rewriting old failed/incomplete results.
|
||||
- Treat separate Firefox prefix3/3 as controlled qualification; original header-onlyFAIL remains separate. No speculative product fix or repeated browser.
|
||||
- Use actual product SHA83fd2495 for ancestry; c95abe22 is a build identifier.
|
||||
- Next recommended investigation is active torrent final-owner disposal under item9/priority2, with real production chain and controlled IO; source hints alone are not reproduced defect or implementation authority.
|
||||
- Notes-only changes need source/hash/whitespace verification, no redundant full/lint/build. Commit evidence locally; no queue submission while source-only routing holds.
|
||||
@@ -0,0 +1,5 @@
|
||||
- Investigation uses actual production chain, fake only observer delivery/WebTorrent IO/timers/persistence; no implementation or committed tests.
|
||||
- Preserve first12=5pass/7fail fixture: two control failures are JSDOM storage-event timers; separate memoryStorage IO fixture12=7pass/5fail retains identical assertions and original evidence.
|
||||
- Treat five failures as planned Stage3 active-cancellation gap, not regression against existing intentional active-sharing/cache contracts. Need explicit routed spec correction before changing those tests.
|
||||
- Smallest fix boundary gridHealth ownership/identity/cache plus probePeers lifecycle only. Preserve deferred temporary-torrent destruction after callback unwinds and release queue slot after teardown; retain shared probe client/other owners.
|
||||
- No live browser/WebTorrent/network/CPU/RangeGET claim; no product edits until independent routing.
|
||||
@@ -0,0 +1,6 @@
|
||||
- Grid publishes owner/release/inflight promise before synchronous client IO so disposal or same-source registration inside constructor/add is safe; cancelled identity removed synchronously and cache guarded.
|
||||
- Optional third probePeers lifecycle follows existing owner-record convention; standalone behavior unchanged. Cancel settlesnull after deferred temporary-torrent destroy call returns/throws; timers clear immediately, cancel hook/owned listeners removed at final teardown.
|
||||
- Retain inert error handler through deferred teardown, even cancellation insideadd; repeated/late callbacks are settled guards. Shared probeClient remains alive; queue slot only releases after destroy, preserving event unwind/cap4/FIFO.
|
||||
- Original tests367/417 contract corrected only after routed spec+integrity note; preserve separate shared-live/cache/TTL/timeout controls and strict noobsoletecache/freshrequest/resource assertions.
|
||||
- No live WebTorrent transport/byte teardown qualification from fake IO. Async low-level network shutdown not measured; existing teardown call ordering preserved.
|
||||
- Stop at completedfocused boundary for requested kit restart; no commit/merge/submission before restart, all snapshots saved.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Client10 decisions
|
||||
|
||||
- Extract only existing animationcleanup block into59line ui/fadeInCleanup helper; index1456→1405lines. No other bootstrap/CSS/app files changed. Existing thumbnail eligibility retained on end/cancel; pseudo effects are excluded from element cleanup.
|
||||
- Install one capture listenerpair then query Document.getAnimations once for already-finished own-target interface CSS animations. Skip running/paused/idle effects and finished effects with same-target live counterpart. No elapsedtime guesses/timers/MutationObserver/layout reads/promises to retain.
|
||||
- Unsupported animation inspection retains event-based cleanup; reducedmotion animation:none retains original hooks without animation effects or new motion. No unsupported-browser late-recovery guarantee. Reference https://www.w3.org/TR/web-animations-1/ describes relevant finished/filling effects and stacking contexts.
|
||||
- Per-document WeakMap makes repeated init idempotent and gives safe explicit disposal/reinstall. Stale disposer cannot erase replacement registration.
|
||||
- Focused scenarios use real helper/JSDOM and controlled CSSAnimation metadata; wiring executes real isolated indexcleanup statements with actual imported helper, not fullapp/bootstrap dependencies. Native CSS animation/hit-testing requires later granted QA. Preserve originaloverflow362/360+394/390 and live/wholeStage3 boundaries.
|
||||
@@ -0,0 +1,522 @@
|
||||
# BitVid BitUnlock permission-layer plan
|
||||
|
||||
Status: proposed application architecture and implementation plan, 2026-07-23.
|
||||
|
||||
BitVid should evolve from a public Nostr video client into a creator-controlled media platform that
|
||||
supports public media, purchases, rentals, creator memberships, platform subscriptions, bundles, and
|
||||
pay-per-view access. BitUnlock is the reusable payment-verification and permission layer beneath those
|
||||
features.
|
||||
|
||||
The architectural rule is:
|
||||
|
||||
> **BitVid owns media behavior. BitUnlock owns payment-backed permission.**
|
||||
|
||||
Related projects and documents:
|
||||
|
||||
- BitUnlock `docs/STREAMING_SUBSCRIPTION_ACCESS_PROFILE.md` — generic entitlement and streaming-access profile.
|
||||
- BitUnlock `docs/GATED_ACCESS_PLATFORM_PLAN.md` — renewable/revocable entitlements and relying-party verification.
|
||||
- BitUnlock `docs/ACCESS_GRANTS_PLAN_V2.md` — bring-your-own-storage capability patterns.
|
||||
- BitUnlock `docs/SECURITY_AUDIT_AND_RESILIENCE_PLAN.md` — security, migration, backup, and recovery gates.
|
||||
- BitVid [`nostr-event-schemas.md`](./nostr-event-schemas.md) — current NIP-71 event behavior.
|
||||
- BitVid [`playback-fallback.md`](./playback-fallback.md) — current URL/WebTorrent playback behavior.
|
||||
|
||||
## 1. Product direction
|
||||
|
||||
BitVid remains a Nostr-native, creator-controlled video application, but its access model expands from
|
||||
"public or hidden" into a general policy system.
|
||||
|
||||
BitVid should support:
|
||||
|
||||
- public videos;
|
||||
- login-only videos;
|
||||
- permanent individual purchases;
|
||||
- time-limited rentals;
|
||||
- creator memberships;
|
||||
- platform-wide subscription catalogs;
|
||||
- bundles, seasons, courses, and collection passes;
|
||||
- pay-per-view premieres and livestreams;
|
||||
- patron/supporter tiers;
|
||||
- free previews and delayed public release;
|
||||
- future prepaid or metered viewing where economically appropriate.
|
||||
|
||||
A single video may accept several routes. For example, a viewer may qualify through an individual
|
||||
purchase, an active rental, a creator membership, or a platform subscription.
|
||||
|
||||
## 2. Responsibility boundary
|
||||
|
||||
### BitVid owns
|
||||
|
||||
- NIP-71 publishing and compatibility;
|
||||
- channels, catalogs, playlists, seasons, bundles, and collections;
|
||||
- search, discovery, feeds, recommendations, and moderation;
|
||||
- profiles, watch history, favorites, and parental controls;
|
||||
- access-policy authoring and resource-to-product mapping;
|
||||
- checkout presentation inside the BitVid UI;
|
||||
- playback-session creation and renewal;
|
||||
- HLS/DASH manifests, MP4 range behavior, WebTorrent policy, storage, CDN, and origin integration;
|
||||
- devices, concurrent streams, household rules, and application sessions;
|
||||
- creator dashboards, pricing presentation, support, refunds, and catalog management.
|
||||
|
||||
### BitUnlock owns
|
||||
|
||||
- product registration and policy discovery;
|
||||
- Lightning/NWC payment-plan creation;
|
||||
- authoritative settlement verification;
|
||||
- partial-payment recovery, retries, and idempotency;
|
||||
- fulfillment and buyer-bound signed entitlement issuance;
|
||||
- perpetual and term access semantics;
|
||||
- renewal, expiration, revocation, and buyer recovery;
|
||||
- generic relying-party verification;
|
||||
- optional storage-capability issuance for static sellers;
|
||||
- SDK, schemas, vectors, sandbox, and normalized errors.
|
||||
|
||||
### BitRoad's role
|
||||
|
||||
BitRoad may list and sell the same products, but BitRoad is not a runtime dependency for playback.
|
||||
Purchases made through BitRoad and purchases initiated inside BitVid should produce compatible
|
||||
BitUnlock entitlements that any compatible BitVid client can verify.
|
||||
|
||||
## 3. Access modes
|
||||
|
||||
### 3.1 Public
|
||||
|
||||
The video is playable by any NIP-71 client. No BitUnlock product is required.
|
||||
|
||||
### 3.2 Login-only
|
||||
|
||||
The video remains free, but BitVid requires a proven Nostr identity before playback. This can support
|
||||
private communities or account features without payment.
|
||||
|
||||
### 3.3 Permanent purchase
|
||||
|
||||
The viewer buys a video or catalog item once and receives a perpetual BitUnlock entitlement. BitVid
|
||||
may issue short-lived playback sessions each time, but the entitlement itself does not expire.
|
||||
|
||||
### 3.4 Rental
|
||||
|
||||
The viewer buys a term entitlement. The first MVP should use purchase-relative expiry because that is
|
||||
already supported by BitUnlock. A future first-play-relative rental requires a separate activation
|
||||
record and must not be inferred from payment alone.
|
||||
|
||||
### 3.5 Creator membership
|
||||
|
||||
A creator defines one or more membership products. BitVid maps each membership tier to a changing set
|
||||
of videos and benefits.
|
||||
|
||||
Example tiers:
|
||||
|
||||
- Supporter — early access and badge;
|
||||
- Member — premium catalog;
|
||||
- Producer — premium catalog, downloads, and private livestreams.
|
||||
|
||||
BitUnlock verifies the tier entitlement; BitVid owns the current benefit mapping.
|
||||
|
||||
### 3.6 Platform subscription
|
||||
|
||||
BitVid defines platform-wide plans such as Basic, Premium, Family, Sports, or Annual. The active plan
|
||||
entitlement grants access to application-defined catalog tiers.
|
||||
|
||||
BitUnlock does not need to know which videos are in each tier. BitVid resolves the catalog policy.
|
||||
|
||||
### 3.7 Bundle or catalog pass
|
||||
|
||||
One entitlement may unlock a season, trilogy, course, conference archive, creator bundle, or topical
|
||||
collection.
|
||||
|
||||
### 3.8 Pay-per-view and livestream admission
|
||||
|
||||
A term entitlement may authorize a premiere, live event, conference, or limited screening, with an
|
||||
optional replay window.
|
||||
|
||||
### 3.9 Delayed public release
|
||||
|
||||
A video may initially require an entitlement and become public at a specified time. BitVid evaluates
|
||||
the release timestamp before invoking paid-access checks.
|
||||
|
||||
### 3.10 Metered viewing
|
||||
|
||||
Pay-per-minute or pay-per-stream is deferred until BitUnlock supports an appropriate prepaid rail and
|
||||
batched fee accounting. BitVid should not create a Lightning invoice or minimum service fee for every
|
||||
small playback action.
|
||||
|
||||
## 4. Resource access policy
|
||||
|
||||
BitVid should represent access as an application-owned OR-of-routes policy.
|
||||
|
||||
Conceptual shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "bitvid-access-policy-v1",
|
||||
"mode": "any",
|
||||
"routes": [
|
||||
{
|
||||
"kind": "public-after",
|
||||
"at": 1798761600
|
||||
},
|
||||
{
|
||||
"kind": "bitunlock-entitlement",
|
||||
"servicePubkey": "<pinned-service-pubkey>",
|
||||
"productCoordinate": "<individual-purchase>"
|
||||
},
|
||||
{
|
||||
"kind": "bitunlock-entitlement",
|
||||
"servicePubkey": "<pinned-service-pubkey>",
|
||||
"productCoordinate": "<creator-membership>",
|
||||
"requireCurrentStatus": true
|
||||
},
|
||||
{
|
||||
"kind": "bitunlock-entitlement",
|
||||
"servicePubkey": "<pinned-service-pubkey>",
|
||||
"productCoordinate": "<platform-subscription>",
|
||||
"requireCurrentStatus": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The exact wire format must be reviewed before implementation. The policy must support:
|
||||
|
||||
- multiple acceptable entitlement products;
|
||||
- public or login-only fallback;
|
||||
- local release times;
|
||||
- perpetual versus term expectations;
|
||||
- mandatory online revocation checks where needed;
|
||||
- future AND conditions for add-ons or region/catalog constraints;
|
||||
- explicit service-pubkey pinning and versioning.
|
||||
|
||||
## 5. NIP-71 compatibility
|
||||
|
||||
BitVid must preserve ordinary NIP-71 interoperability.
|
||||
|
||||
Recommended approach:
|
||||
|
||||
1. Keep the standard NIP-71 video event valid and useful to other clients.
|
||||
2. Add an optional reference to a separate replaceable BitVid access-policy event, or a minimal
|
||||
extension tag pointing to that policy.
|
||||
3. Do not publish permanent private storage URLs, decryption keys, playback tokens, buyer identities,
|
||||
or subscription state in the public video event.
|
||||
4. Clients that do not understand the access extension may show metadata and a locked state rather
|
||||
than receiving a false public playback URL.
|
||||
5. Public videos continue to carry normal public playback sources.
|
||||
|
||||
A separate access-policy event is preferable to embedding a large mutable catalog rule directly in
|
||||
every video event. It allows membership/catalog mappings to evolve without republishing media
|
||||
metadata.
|
||||
|
||||
## 6. Viewer flow
|
||||
|
||||
### 6.1 Discovery
|
||||
|
||||
BitVid loads the NIP-71 event and associated access policy. It can render the card, title, creator, and
|
||||
preview metadata without revealing protected media capabilities.
|
||||
|
||||
### 6.2 Authentication
|
||||
|
||||
The viewer signs in through BitLogin, NIP-07, or another supported Nostr signer. BitVid treats the
|
||||
proven pubkey as the account identity.
|
||||
|
||||
### 6.3 Entitlement recovery
|
||||
|
||||
BitVid loads buyer-held entitlements from:
|
||||
|
||||
- local encrypted storage;
|
||||
- the buyer's relay/vault recovery path;
|
||||
- BitUnlock's authenticated buyer-recovery endpoint.
|
||||
|
||||
The client should cache receipts but re-evaluate expiry and required revocation status before granting
|
||||
new playback.
|
||||
|
||||
### 6.4 Policy evaluation
|
||||
|
||||
BitVid checks local routes first, then verifies candidate BitUnlock receipts against:
|
||||
|
||||
- pinned BitUnlock service pubkey;
|
||||
- authenticated buyer pubkey;
|
||||
- expected product coordinate;
|
||||
- supported schema and policy versions;
|
||||
- entitlement term and expiry;
|
||||
- current revocation status where required.
|
||||
|
||||
### 6.5 Checkout
|
||||
|
||||
When no route succeeds, BitVid presents applicable purchase choices inside the video UI:
|
||||
|
||||
- buy permanently;
|
||||
- rent;
|
||||
- join creator membership;
|
||||
- subscribe to BitVid plan;
|
||||
- buy event pass.
|
||||
|
||||
The BitUnlock SDK manages quote creation, wallet interaction, partial-payment recovery, bounded
|
||||
polling, unlock, and entitlement validation.
|
||||
|
||||
### 6.6 Playback authorization
|
||||
|
||||
After a route succeeds, a trusted BitVid backend or media-origin component issues a short-lived
|
||||
resource-scoped playback capability. The video bytes flow from the storage/CDN/origin to the viewer,
|
||||
not through BitUnlock.
|
||||
|
||||
## 7. Playback architecture
|
||||
|
||||
### 7.1 MVP private-origin path
|
||||
|
||||
Use a small BitVid authorization service or edge function:
|
||||
|
||||
1. Receive authenticated viewer proof and buyer-presented entitlement.
|
||||
2. Verify the entitlement with the BitUnlock SDK.
|
||||
3. Evaluate the BitVid resource policy.
|
||||
4. Create a short-lived playback session.
|
||||
5. Return a signed manifest URL, signed cookie, or presigned MP4/object URL.
|
||||
6. Refresh the session during long playback without requiring a new payment.
|
||||
|
||||
### 7.2 Capability properties
|
||||
|
||||
The playback capability should bind:
|
||||
|
||||
- buyer or delegated device identity;
|
||||
- video/resource ID;
|
||||
- accepted policy route and entitlement ID;
|
||||
- origin/audience;
|
||||
- issue and expiry time;
|
||||
- session revision or nonce;
|
||||
- plan quality/download permissions where applicable.
|
||||
|
||||
The capability should normally last minutes, even when the subscription lasts a month.
|
||||
|
||||
### 7.3 HLS/DASH
|
||||
|
||||
The design must account for:
|
||||
|
||||
- manifest authorization;
|
||||
- segment requests after the original URL expires;
|
||||
- session renewal during long playback;
|
||||
- seeking and bitrate switching;
|
||||
- CDN caching without making protected manifests public;
|
||||
- signed cookies versus per-segment query tokens;
|
||||
- CORS and CSP among BitVid, the authorization service, and the media origin.
|
||||
|
||||
### 7.4 MP4
|
||||
|
||||
For MP4 playback, preserve byte-range support and ensure session renewal does not break seeking. A
|
||||
short-lived signed cookie or origin session may work better than a one-time URL with an expiry shorter
|
||||
than the video.
|
||||
|
||||
### 7.5 WebTorrent
|
||||
|
||||
Public WebTorrent distribution is naturally incompatible with revocable private subscription access
|
||||
once plaintext media is shared to peers. BitVid should:
|
||||
|
||||
- keep WebTorrent for public media;
|
||||
- allow encrypted torrent payloads for permanent purchases where the buyer receives a decryption key;
|
||||
- not claim that private subscription streams remain revocable after distributing a reusable
|
||||
decryption key or plaintext torrent.
|
||||
|
||||
## 8. Subscription MVP
|
||||
|
||||
The first subscription release should intentionally avoid automatic charging and household complexity.
|
||||
|
||||
MVP capabilities:
|
||||
|
||||
- one creator monthly membership product;
|
||||
- one BitVid platform monthly plan;
|
||||
- manual Lightning renewal;
|
||||
- current plan and expiry display;
|
||||
- renewal checkout against the same product;
|
||||
- immutable new entitlement for each period;
|
||||
- expiration and seller revocation;
|
||||
- catalog-tier mapping;
|
||||
- recovery on a new device;
|
||||
- one active viewer session policy;
|
||||
- short-lived playback session against a private test origin.
|
||||
|
||||
The MVP is complete when a buyer can subscribe, recover the entitlement elsewhere, play an included
|
||||
video, fail after expiry/revocation, renew, and play again without mutating the old receipt.
|
||||
|
||||
## 9. Automatic renewal
|
||||
|
||||
Automatic renewal is a later phase requiring an explicit, constrained buyer NWC authorization.
|
||||
|
||||
Requirements include:
|
||||
|
||||
- merchant and product binding;
|
||||
- maximum amount;
|
||||
- minimum billing interval;
|
||||
- period and cumulative spending limits;
|
||||
- cancellation and revocation;
|
||||
- duplicate-charge and replay protection;
|
||||
- notifications and failed-renewal behavior;
|
||||
- no seller ability to broaden permission silently.
|
||||
|
||||
BitVid must not market subscriptions as automatically recurring until this separate authorization flow
|
||||
has been implemented and reviewed.
|
||||
|
||||
## 10. Devices, profiles, and household access
|
||||
|
||||
The root Nostr key should not be copied to every television.
|
||||
|
||||
Later phases should introduce:
|
||||
|
||||
- buyer-authorized device keys or application sessions;
|
||||
- device list and remote sign-out;
|
||||
- delegated scope limited to BitVid;
|
||||
- maximum devices per plan;
|
||||
- concurrent stream limits;
|
||||
- profile records and parental controls;
|
||||
- household-member authorization without sharing the root key;
|
||||
- revocation and recovery.
|
||||
|
||||
Profiles and household rules remain BitVid application state. BitUnlock only verifies the underlying
|
||||
subscription entitlement unless a future generic delegated-capability feature is intentionally added.
|
||||
|
||||
## 11. Creator and operator experience
|
||||
|
||||
Creators should be able to:
|
||||
|
||||
- register BitUnlock-backed purchase, rental, membership, and event products;
|
||||
- connect those products to videos, playlists, or catalog groups;
|
||||
- define accepted access routes;
|
||||
- preview the locked viewer experience;
|
||||
- revoke a term entitlement where policy permits;
|
||||
- see aggregate commerce state without receiving buyer private keys or sensitive credentials;
|
||||
- use BitRoad as an optional storefront while keeping BitVid-native checkout available.
|
||||
|
||||
Operators should be able to:
|
||||
|
||||
- configure the accepted BitUnlock service identity;
|
||||
- configure a private media origin/CDN;
|
||||
- control playback-session TTLs and concurrent-session policy;
|
||||
- suspend new paid access safely during incidents;
|
||||
- audit authorization failures without logging receipts or playback tokens.
|
||||
|
||||
## 12. Privacy and security
|
||||
|
||||
- Buyer receipts should remain private and buyer-presented.
|
||||
- BitVid must not expose a public API allowing arbitrary enumeration of buyer purchases.
|
||||
- Playback tokens, signed URLs, entitlements, NWC URIs, and storage credentials must not enter logs.
|
||||
- Client-side verification may improve UX but cannot be the sole control for a private media origin.
|
||||
- Copied receipts must fail for a different authenticated pubkey.
|
||||
- Valid receipts for unaccepted products must fail.
|
||||
- Expired and revoked subscriptions must not mint new sessions.
|
||||
- Cross-video and cross-channel capability substitution must fail.
|
||||
- BitUnlock's service identity and accepted schema versions must be pinned.
|
||||
- BitUnlock must never proxy video bytes.
|
||||
- Private storage/CDN credentials must remain application-side or narrowly scoped and encrypted.
|
||||
|
||||
## 13. Implementation phases
|
||||
|
||||
### Phase 0 — Architecture and protocol decisions
|
||||
|
||||
- [ ] Freeze the BitVid responsibility boundary with BitUnlock and BitRoad.
|
||||
- [ ] Define `bitvid-access-policy-v1` and negative vectors.
|
||||
- [ ] Decide the NIP-71 extension/reference format without breaking ordinary clients.
|
||||
- [ ] Define resource IDs, catalog groups, product coordinates, and policy versioning.
|
||||
- [ ] Define public, login-only, permanent, rental, membership, subscription, bundle, event, and
|
||||
delayed-public routes.
|
||||
- [ ] Threat-model buyer receipt privacy, copied receipts, cross-resource substitution, and playback
|
||||
capability leakage.
|
||||
|
||||
### Phase 1 — BitUnlock SDK and identity integration
|
||||
|
||||
- [ ] Pin BitUnlock service policy and service pubkey.
|
||||
- [ ] Add BitLogin/Nostr authenticated buyer context where not already available.
|
||||
- [ ] Integrate entitlement recovery.
|
||||
- [ ] Integrate `verifyEntitlement()` and revocation/status checks.
|
||||
- [ ] Add typed access-decision results and normalized user-facing errors.
|
||||
- [ ] Add unit tests for wrong buyer, wrong product, bad signature, expiry, and revocation.
|
||||
|
||||
### Phase 2 — Locked media UX and checkout
|
||||
|
||||
- [ ] Render locked cards and video pages without exposing protected playback URLs.
|
||||
- [ ] Show accepted purchase/rental/subscription choices.
|
||||
- [ ] Integrate BitUnlock checkout, partial-payment recovery, and completed-purchase recovery.
|
||||
- [ ] Display current entitlement route, expiry, and renewal state.
|
||||
- [ ] Accept entitlements produced by BitRoad purchases.
|
||||
- [ ] Add sandbox fixtures for each access mode.
|
||||
|
||||
### Phase 3 — Playback authorization service
|
||||
|
||||
- [ ] Create a minimal trusted BitVid authorization service/edge function.
|
||||
- [ ] Verify buyer authentication and presented entitlements server-side.
|
||||
- [ ] Evaluate `bitvid-access-policy-v1`.
|
||||
- [ ] Issue short-lived resource-scoped playback sessions.
|
||||
- [ ] Integrate a private test origin with signed URL/cookie support.
|
||||
- [ ] Add renewal during long playback and safe error recovery.
|
||||
- [ ] Test cross-video, replay, expiry, and token-leak scenarios.
|
||||
|
||||
### Phase 4 — Subscription MVP
|
||||
|
||||
- [ ] Register one creator membership and one platform subscription product.
|
||||
- [ ] Map subscription products to catalog groups.
|
||||
- [ ] Add manual renewal UX.
|
||||
- [ ] Demonstrate expiry, revocation, renewal, and new-device recovery.
|
||||
- [ ] Add a subscription-management view.
|
||||
- [ ] Enforce an initial concurrent-session policy.
|
||||
- [ ] Complete a real mainnet end-to-end subscription and playback test after sandbox proof.
|
||||
|
||||
### Phase 5 — Purchases, rentals, bundles, and events
|
||||
|
||||
- [ ] Permanent individual video purchase.
|
||||
- [ ] Purchase-relative rental.
|
||||
- [ ] Bundle/season/course pass.
|
||||
- [ ] Pay-per-view and replay window.
|
||||
- [ ] Delayed public release.
|
||||
- [ ] Creator tier benefits beyond playback.
|
||||
|
||||
### Phase 6 — Production streaming
|
||||
|
||||
- [ ] Choose HLS/DASH/CDN architecture.
|
||||
- [ ] Implement manifest and segment authorization.
|
||||
- [ ] Preserve MP4 range and seek behavior.
|
||||
- [ ] Define CORS, CSP, cache, and origin rules.
|
||||
- [ ] Add playback-session observability without leaking viewing details.
|
||||
- [ ] Load-test authorization and renewal paths.
|
||||
- [ ] Add incident suspension and recovery procedures.
|
||||
|
||||
### Phase 7 — Devices and households
|
||||
|
||||
- [ ] Delegated device keys or application sessions.
|
||||
- [ ] Device list, revocation, and remote sign-out.
|
||||
- [ ] Concurrent-stream limits by plan.
|
||||
- [ ] Profiles and parental controls.
|
||||
- [ ] Household-member authorization.
|
||||
- [ ] Recovery without copying the root Nostr private key.
|
||||
|
||||
### Phase 8 — Advanced billing
|
||||
|
||||
- [ ] Design and audit restricted automatic NWC renewal.
|
||||
- [ ] Trials, promotions, upgrades, downgrades, and plan transitions.
|
||||
- [ ] Cashu/prepaid or batched metered viewing after BitUnlock fee accrual is ready.
|
||||
- [ ] Creator revenue and platform-fee reporting without making BitVid custodial.
|
||||
|
||||
## 14. Acceptance criteria
|
||||
|
||||
The first complete reference implementation must demonstrate:
|
||||
|
||||
1. A standard public NIP-71 video still works in ordinary clients.
|
||||
2. A protected video reveals no permanent private-media URL publicly.
|
||||
3. A buyer purchases a monthly BitUnlock subscription inside BitVid.
|
||||
4. An independently versioned BitVid authorization component verifies the entitlement.
|
||||
5. The component issues a five-minute capability for the correct video.
|
||||
6. The media origin serves bytes directly without BitUnlock proxying them.
|
||||
7. A copied entitlement fails for another pubkey.
|
||||
8. A valid but unrelated entitlement fails.
|
||||
9. Expiry and revocation prevent new playback sessions.
|
||||
10. Manual renewal creates a new entitlement period and restores access.
|
||||
11. The buyer recovers access on a new device without repaying.
|
||||
12. An individual permanent purchase is accepted as an alternate route to the same video.
|
||||
13. A BitRoad-originated purchase works without BitRoad being present during playback.
|
||||
14. Logs and analytics contain no entitlement plaintext, NWC secrets, storage credentials, or
|
||||
playback bearer tokens.
|
||||
|
||||
## 15. Explicit non-goals for the first release
|
||||
|
||||
- Automatic recurring charges.
|
||||
- Full Netflix-style household detection.
|
||||
- Strong DRM guarantees against screen capture or authorized-user copying.
|
||||
- Private plaintext WebTorrent subscription streaming.
|
||||
- Pay-per-minute billing.
|
||||
- Geographic licensing enforcement.
|
||||
- Recommendation-system redesign.
|
||||
- Building BitUnlock-specific payment or entitlement logic directly into BitVid instead of using the
|
||||
public SDK and generic contracts.
|
||||
@@ -4,6 +4,7 @@ Welcome to the bitvid documentation. This directory contains detailed architectu
|
||||
|
||||
## Core Systems
|
||||
|
||||
- **[BitUnlock Permission Layer](./BITUNLOCK_PERMISSION_LAYER_PLAN.md)**: Architecture and phased plan for purchases, rentals, creator memberships, platform subscriptions, bundles, events, and short-lived playback authorization using BitUnlock as BitVid's permission layer.
|
||||
- **[Application Coordinators](./app-coordinators.md)**: Overview of the coordinator pattern used to manage app-wide state and services.
|
||||
- **[Data-Flow Refactor — Dev Plan & Spec](./architecture-refactor.md)**: The incremental plan to put all subsystems on a shared, governed relay/data foundation (SubscriptionManager, stores, unidirectional flow).
|
||||
- **[Known Bugs (deferred)](./KNOWN_BUGS.md)**: Confirmed defects/limitations found during the refactor, queued to fix later.
|
||||
@@ -30,10 +31,28 @@ Welcome to the bitvid documentation. This directory contains detailed architectu
|
||||
- **[Design System](./design-system.md)**: Guide to the token-based design system and CSS architecture.
|
||||
- **[Menus & Popovers](./menus.md)**: Documentation for the menu and popover system.
|
||||
|
||||
## Testing
|
||||
|
||||
- **[Playwright Agent Testing Infrastructure](./testing/agent-playwright-harness.md)**: Full reference for the agent test harness (`window.__bitvidTest__`), mock relay, Playwright fixtures, and `data-testid` selectors.
|
||||
- **[Test Integrity (Dark Factory Standard)](./testing/test-integrity.md)**: The scenario-first testing constitution — anti-cheat rules, the Spec Correction Protocol, and the Test Integrity Note format.
|
||||
|
||||
## Lessons & Incident Writeups
|
||||
|
||||
- **[Cloudflare/S3 Upload-Path Gotchas](./lessons/cloudflare-s3-upload-gotchas.md)**: Audited 2026-06-16 — duplicated upload services, storage-key collisions, and object-cleanup contracts.
|
||||
- **[NIP-07 Signer Reliability](./lessons/nip07-signer-reliability.md)**: Hard-won 2026-06-16 — diagnosing unresponsive NIP-07 signers and the resilience invariants that must not regress.
|
||||
|
||||
## Operations
|
||||
|
||||
- **[Instance Configuration](./instance-config.md)**: Guide to configuring a bitvid instance.
|
||||
- **[Deployment Notes](./deployment-notes.md)**: Notes and checklists for deploying bitvid.
|
||||
- **[Manual QA Checklist](./qa.md)**: The standard manual QA script for releases.
|
||||
|
||||
## Active plans
|
||||
|
||||
- **[Performance & Efficiency Plan](./performance-efficiency-plan.md)**: A
|
||||
measurement-first staged plan for reducing client CPU, memory, and network
|
||||
work without degrading Nostr discovery, feed freshness, moderation, or
|
||||
playback. Tracked as TODO #34.
|
||||
- **[BitUnlock Permission-Layer TODO](../todo/TODO_2026-07-23_bitunlock-permission-layer.md)**: Ordered implementation checklist for the BitUnlock-backed media permission and subscription work.
|
||||
|
||||
For more details on contributing, please see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
# TODO — what each area did NOT finish
|
||||
|
||||
## dispatch
|
||||
|
||||
- 2026-10-23 buildc95abe22/artifactpass at103eb04c;SWcompatcheckSKIPPED Gitdelta unavailable,retainlimit. HLSevidencea482d9d2 independentlydurable. QA4 realappready360,Uploadclickblockedfirst-run disclaimer30s,no geometry/focus/touch;sourceexactinterceptclass maps components/disclaimer.html18,runneromitsaccept. QA genuineUIacceptpreponly/no productbugclaim; HLSnativebrowser solegrant13:04:31. FreshpowerACADonline1/BAT100%Discharging;no lowbatterythreshold,resourceadvisoryreviewed.
|
||||
|
||||
- 2026-10-23 HLS product83fd2495 durable independentlystatus/log/ancestry; current savedfulllog independently437starts/437unique,lastzap-totals;normalgateexit0. StorageRangeaudit103eb04c durable. Client solebuild/artifact granted; QA4 conditionalhostbrowser afterbuildrelease+freshhealth, HLSnativebrowser later. Priorpublish/thermalrejections+398partial+firstfull-logloss preserved. Torrent/Rangeimplementation/FirefoxCI/Amber/mobile/previewintegration remainopen.
|
||||
|
||||
- 2026-10-23 HLS replacement280f043a durablyREJECTED after resource-triggered own-gateSIGINT, worker sustained88.8→85.1C and398/437unique partial/lastvideo-modal-controllers interrupted. No fullgreen/productassertionfailure claim. Interpreter kitRELEASE12:46:56 reports84pass/9guards/upstreambeta-onlypush; exactstart/end requested to assess overlap before nextlonggate. No HLSretry/build/browser until cooling/no competingjob assessment; normalgate unchanged/required.
|
||||
|
||||
- 2026-10-23 resume: actual QA4 bootEPERM/release consumed; browser/runtimechecks0, prior mobile strict failures unchanged. HLS sole full/normal grant12:32:43Z at clean79a9a130 after safe merge latest672ca953; START/results pending. Health cooling77→71C/RAM68.3%/swap90% advisory; one heavy max. QA host escalation disposition/browser grant later; no app-fix/cron/autoapproval-effect claim. Overnight client wake left composer yet unread55642s, recovery now consumed.
|
||||
|
||||
- 2026-10-22 storage3+4DONE: storage4 product7edc7ea8/closure8317da4d independently durable status/log/ancestry; worker normalgateexit0/discovery436/build29cbe6f0 artifactPASS, no independent436start audit. QA4 now sole bounded mobile browser grant21:04:02; HLS candidatecdc398b0/merged79a9a130 next full after QA release. Dispatch exact candidate35/35 natural321ms; worker129focused/19mutants/lint11. HLS gates/preview/campaign remain unfinished.
|
||||
|
||||
- 2026-10-22 HLS follow-on: dispatch independent actual observer/controller/helper scratch atd04d501e12=7pass/5fail/cancelled0/naturalexit1/165.17ms. Final-owner GET/timer/promise retained, rejoin stale, late healthy cache, body-read disposal fail; no media-slot leak. Controller HLS handoff + optional manifest-helper cancellation only routed with strict spec/integrity/regressions. Focused/mutants/lint only while storage4 holds heavy slot; torrent/RangeGET remain separate, no HLS fix landed.
|
||||
|
||||
- 2026-10-22 HEAD slice productf41ac656/evidenced04d501e durable status/log/ancestry verified. Worker normal435unique/buildartifact/nativeChromium+Firefox controlledHEAD pass; loadeddata synthetic, no decoder/transport-byte proof. All client resources released20:51:23Z. Storage4 sole full/normal START ACK20:52:16; client9 HLS read-only measurement ACK, broader9 remainsOPEN; QA4 HOLD. Integrated preview and other campaign priorities remain unfinished.
|
||||
|
||||
- 2026-10-22 client9 product normally landedf41ac656: dispatch durable status/log/ancestry0, worker gateexit0/435unique starts. Sequential build active, separate bounded HEAD browser after build conditionally allocated; results pending. Storage4 candidateb21ce86c focused16/9mutants/lint11 ready, next full slot after client release; QA4 held. Integrated preview and broader campaign gates unfinished.
|
||||
|
||||
- 2026-10-22 restart20:41UTC: a79d49df verified durably landed26e9a787, queue0. Fleet health cooling74 to66C/load3.81 to3.30, >20GiB available RAM, swap52 to51% sole suggested-limit crossing. Client9 sole full/normal slot granted; START/results pending. Storage4/QA4 held. Own sync blocked read-only Git metadata; no new product/tests/build/browser qualification.
|
||||
|
||||
- 2026-10-22 storage4 registeredfeed/JSDOM independent4=2pass/2fail:obsolete result restores deletedDOM/overwrites replacementExplore;coordinator-only separate fix routed afterstorage3 submit. Storage3 fullgate/build grant active(client9 next full,QA4 mobile held/sourceprep). Client9 exactcandidate independent23pass. No product landing yet.
|
||||
|
||||
- 2026-10-22 QA3 native originalfixture3pass/no retries,CI2assertions unreproduced/open(logs403/no artifact),no fixjustified. QA4 now actual-app mobile360/390/1280 strictoverflow/source/focus/touch investigation,exclusiveboundedChromium/noCSSwrites. Remaining6priority campaign not complete.
|
||||
|
||||
- 2026-10-22 client9 HEAD independently11modes(3fail/8controls pass),finalowner fetch/timer retained(no media slot leak);controller HEAD-only fix+strict contract/identity/ignoringabort regressions routed. HLS/torrent/RangeGET remain open;QA offline genuineBuffer rebuild prep, mobile/Amber staged.
|
||||
|
||||
- 2026-10-22 storage3 independent7/3pass/4fail: buffer newer-version loss and service deleted snapshot/cache leakage routed separate small commits; coordinator stale render still open/read-only pending registered lifecycle evidence. QA3 app-less3pass not CI proof; genuine cached Buffer followup granted exclusive narrow browser.
|
||||
|
||||
- 2026-10-22 resumed six-priority campaign: Firefox QA3, active cancellation client9, tombstones storage3 now; playback dispatch4, Amber dispatch5, mobile dispatch6 staged with acceptance criteria. See docs/agents/CAMPAIGN-2026-10-22.md. All six remain unfinished; old idle/quiet-hours hold superseded for this foreground scope. Final integrated checks/build/preview and live/manual disclosure outstanding.
|
||||
|
||||
- Next bounded item requested by interpreter2026-10-21T02:40:06Z under continuing owner instruction: existing client read-only currentHEAD active HEAD-request last-owner abort reproduction/shared/rejoin controls;no product/test edits before evidence. HLS/torrent separate. Quiet-hours22:00–08:00 hold for new starts/messages;route next foreground checkpoint at/after08:00 local without another permission loop. No scheduler created.
|
||||
|
||||
- client8 FINAL:product43cb1baf/closure2ea6f068/handoff9cbb53f8 verified;432fullgate0,dispatch29/29/build08620a71/artifact0/preview200+cmp0,nativecontrolledbothbrowserPASS/released. AllIDLE;HEAD/HLS/torrent activeprobe work remains unfinished pending nextboundedrouting. No decoder/bytes/CPU/liveproof.
|
||||
|
||||
- 2026-10-20 client8 active URL-media product43cb1baf/full432gate0,dispatch29/29/build08620a71/artifact0/previewHTTP200+cmp0. WorkercontrollednativeChromium+Firefox smokePASS/released;closuremetadata/browsernotes normaldocsqueue pending. No realdecoder/networkbyte/CPU/HEAD/HLS/torrent qualification;broaderactiveprobe scope remains unfinished. Finishdocs/handoff thenawaitnextboundedrouting.
|
||||
|
||||
- 2026-10-20 bounded continuation: client8 active URL-media last-owner/shared/rejoin reproduction after maintenance. Await measured evidence then independent confirmation/minimal fix and explicit test-contract routing. Other optional work remains deferred; no active-abort completion claim.
|
||||
|
||||
- 2026-10-20 LOCAL REVIEW: storage2/history6/cold-feedclient7 normally landed;latest codeb1174163 full432filegate0,dispatchnew7/7/build-artifact0. Preview3000 build6da89895/sourceb1174163/runtime-equivalentdocsdescendants. Actualcold3cardsPASS;separate25-step360/390/1280navigation/Home/Profile/Uploadvalidation/focuscoveragecomplete. OriginalUploadoverflowFAIL remainsopen2/4px(controlsusable/sourceunproven),notwholebaselineallpass. FinalQA/docsqueuehandoff thenallIDLE;nooptionalwork. Feed3/publicplaybackpromises/liveAmber/cloud/peers/manualrelease remainunfinished. See docs/agents/LOCAL-REVIEW.md.
|
||||
|
||||
- 2026-10-20 local-review checkpoint: storage2 producte7b9afb3/closurec5818cfe and history6 regressionb3fab032/closure386cc60e durably complete. Normal latest gate431files exit0; dispatch cleanup13/13 and history3/3. Integrated preview1834b6a9 runtime equals current test/docs-only descendants. Final actual-app/mobile QA2 browser pass active; readiness awaits actual flow evidence/handoff. Feed3 and public-promised playback roadmap deferred/open; real Amber/cloud/peers/release gates remain. No optional new tasks.
|
||||
|
||||
- 2026-10-20 trial outputs complete:QA regression1bb3a4fc/closurebbf57388 verified,dispatch independent4/4/runtime unchanged so no extra build. Same3 workers continue:client6 real history test-only retention after independent current pass/historical-noop failure;storage2 method-only malformed metadata no-throw guard after independent2rejects/6controls;QA2 actual-app local core/mobile3viewports browser-only grant. Full slots on validated-ready after QA browser release;no feature/readiness/live-proof claim.
|
||||
|
||||
|
||||
- 2026-10-20 owner continuous-quality direction recorded;fleet/QUALITY.md tracks local vs real release evidence. QA full/normal slot active;client item6 profile-history wiring audit and storage item2 feed/view tombstone audit assigned now,read-only before fix routing. Remaining:actual coverage/reproduction decisions,current QA landing,local mobile/core-flow baselines and unqualified live gates.
|
||||
|
||||
|
||||
- 2026-10-20 storage productd3f4c215/closure7f99c045 verified;dispatch CORS9/9/build-artifact/previewHTTP200+dist match. QA exclusive full/normal slot actual grant14:49:35/read14:49:46 after65m54wait;storage-release→grant39m57 wake gap. Remaining:QA durable regression landing,final trial throughput/stall assessment,next bounded routing. Actual cloud/IAM/noCAS concurrent safety/live peers unverified.
|
||||
|
||||
|
||||
- 2026-10-20 trial first product landed: item5 product0bd0b1c3,closuredc852597;dispatch lifecycle18/18/build-artifact/previewHTTP200+dist match. Storage slot grant13:58:27 after949s wait,full/normal landing underway;QA next,ready95a77a7f. Remaining: storage/QA durable landings,preview refresh after product changes,final trial evidence;real cloud/peers/activeabort/CPU unverified.
|
||||
|
||||
|
||||
- 2026-10-20 trial current: all3 pickups verified; admin notes durableb922c186. Client item5 owns exclusive full/browser/product landing from13:43:51 (~10m57s wait); storage CORS ready9/9+8mutants,next slot; QA retained4/4+6mutants,third slot,browser released. Remaining: full merged-tree validation,durable product/test landings,preview refresh and final trial outcome. No real cloud/peers/CPU qualification.
|
||||
|
||||
|
||||
- 2026-10-20 parallel trial: rosterbbc1be79 durably green,3 workers launched/read-next-action verified. Item5 ready12f92732 focused28/6mutants/lint/build awaits full+browser; storage-cors real origin loss independently reproduced/fix routed; playback-qa controlled native local fallback3/browser reviewed,test retention approved. Remaining trial product/test full gates,orderly durable landings and preview; actual WebTorrent/live peers/CPU unverified. Initial gate transport/capture failure corrected only by clean environment; QA browser coordination deviation recorded.
|
||||
|
||||
|
||||
- 2026-10-20: continuing owner direction: item5 investigation assigned to same worker, active URL media last-owner/shared-owner controls first. Remaining: reproduction and narrow fix routing or next evidenced TODO. No product fix assumed; active abort/live playback/CPU-RSS unverified.
|
||||
|
||||
|
||||
- 2026-10-20: item4 done: product89ccb907,closuref116f7c8 durable landed. Dispatch independent URL tests20/20,build/artifact exit0,previewHTTP200 and index matches dist. Worker full427/427,focused22,13 mutants,lint11,browser smoke recorded. Worker idle; next active-probe candidate needs fresh reproduction/routing. Actual playback/live peers/CPU-RSS/full Stage3 unfinished.
|
||||
|
||||
|
||||
- 2026-10-20: item4 reproduction confirmed independently: obsolete URL media starts1(expected0), shared owner control pass. Worker routed to queued URL ownership/cancellation in observer/controller and focused tests. Remaining: implementation, mutant/full/browser gates, landing, preview refresh; active abort excluded.
|
||||
|
||||
|
||||
- 2026-10-20: item4 assigned to existing worker for investigation only: reproduce remaining URL/active torrent cancellation at HEAD, then route a narrow fix. No product fix yet; live playback/CPU-RSS remain unverified. Owner continuation relayed; quiet-hours exception does not change schedule.
|
||||
|
||||
|
||||
- 2026-10-19 latest: item3 normal product landing7d20a509 and closure7fde734d verified; dispatch synced and preview rebuilt(HTTP200). Independent original five-card scenario now0 obsolete starts(previous1), lifecycle11/11. Worker evidence426/426 full files,lint11/11,14 rejected mutants,Chromium+Firefox controlled-IO lifecycle smoke. Worker idle; no new task launched in this mail checkpoint. Remaining: actual media fallback/live-peer qualification,CPU/RSS measurements,active-probe/URL cancellation and other performance-plan slices.
|
||||
|
||||
|
||||
- 2026-10-19: dispatch-2 ongoing coordination. Synced to f9adfc8f; normal-gate fixes de6918ce verified (425/425 file evidence, lint11/11, independent profile27/27/race1/1). Route client-reliability-3 to same worker for reproduced queued probe-after-feed-disposal bug; scope/evidence in area prompt. Next: verify actual worker resume and completion reports, keep preview current after verified product landing.
|
||||
- Coordination limitation: operator checkpoint resumed dispatch; file mailbox does not itself wake idle Codex. Completion handoff now explicitly requires mailbox evidence, but unattended dispatch wakeup is unqualified. No scheduler/kit changes at this checkpoint.
|
||||
|
||||
## client-reliability
|
||||
|
||||
- 2026-10-27 client10 DONE: product5b0adf6b normal439gate exit0/ancestry0, exactcandidateQA native4flowPASS peerattributed, build+artifact86fc5236 PASS/helpercmp0. Required build/landing evidence23c69eba durablyaf8e6540/docs-only/ancestry0; closed after durableevidence. AllslotsRELEASED, no artifactpublication/preview/deploy. Prioroverflow362/360+394/390, nativeactivecancel/visualAA/item9live/CPU/wholeStage3 remain separate/open. No new task/runtime self-selected.
|
||||
|
||||
- 2026-10-27 01:54UTC: client10 separate localbuild+artifactverifierPASS/naturalexit0/version86fc5236/indexSHA50a89bd6/helperdistbyteequalcmp0. No trackedartifactdelta/preview/deploy. SolebuildslotRELEASED; land build+normal439gate evidence docs-only then close10; historicaloverflow/nativeactivecancel/AA/item9live limits retained. test_logs/TEST_LOG_2026-10-27_01-54-00_client-reliability-10-build.md.
|
||||
|
||||
- 2026-10-27 01:52UTC: client10 productLANDED5b0adf6b via exactaff88416 normal439gate exit0/discovery439unique/missingextra0/ancestry0. QA exactcandidate nativepoint acceptance PASS attributeddispatch/QA; alloldoverflow/activecancelnative/AA/live limits retained. Completegateevidence state/evidence/client10-animation-cleanup-20261027; slotRELEASED. Separate remainingbuild/artifact/native routing pending, item10open. test_logs/TEST_LOG_2026-10-27_01-52-00_client-reliability-10-landed.md.
|
||||
|
||||
- 2026-10-27 client10 scoped cleanup READY: late-init recovery extracted from index animation block to59line helper;17newscenarios pass/19distinct killedmutants/all17covered/lint11pass. Existing tests/CSS/app/bootstrap untouched outside block/import. No full/build/browser/submission grant; native360point+390/1280/reducedmotion and normalgate/landing remain. Preserve prior362/360+394/390 overflow and item9live/native limits. Evidence test_logs/TEST_LOG_2026-10-27_01-39-00_client-reliability-10-ready.md.
|
||||
|
||||
- 2026-10-26 19:43UTC: separately granted local build/artifact passes exit0 at7594c54d (runtimeaf61c271); artifact15868a91/indexSHA9c48e0fe, clean tree, no previewserver/deploy. Sandbox build swcompat history probe skipped; prior unchanged-input hostlint11pass retained. Slot RELEASED; native torrent/browser+QA timing need distinct routing, item9 staysopen. test_logs/TEST_LOG_2026-10-26_19-43-00_client-reliability-9-build.md.
|
||||
|
||||
- 2026-10-26 19:41UTC: item9 active-torrent product LANDED af61c271 via normal eafa364d queue gate exit0; independent438discovered/438unique starts/missingextra0 and ancestry0. Complete log/result/discovery hashed under state/evidence/client9-torrent-validation-20261026, prior rejected275partial intact. lint11/savedfocused60/mutants19 retained. Normalgate slot RELEASED; separate build/native-browser routing remains, item9 open. No real peers/byte teardown/wholeStage3 claim. See test_logs/TEST_LOG_2026-10-26_19-41-00_client-reliability-9-torrent-landed.md.
|
||||
|
||||
- 2026-10-26 19:16UTC: item9 full lint11PASS; normal queue c20635bc interrupted for sustained90.1C→86C over38s. Durable rejected/red exit1, unstable04c8c7cf unchanged, not landed. Partial275/438starts/275unique,163notstarted; original log/result preserved in state/evidence/client9-torrent-validation-20261026. Slot RELEASED, fresh resource+new gate grant required; no auto-resubmit/build/browser. Evidence test_logs/TEST_LOG_2026-10-26_19-16-00_client-reliability-9-thermal-interruption.md.
|
||||
|
||||
- 2026-10-26 resumed item9 active-torrent candidate `1b2362d2`, merged unstable `04c8c7cf` at `1bf50c1f`; exact saved hashes/focused60/60/19distinct killed mutants preserved. Remaining full lint/full/build/browser/normal landing require dispatch slot plus fresh health; TCPU97.1C at19:07:29 exceeds85C. No new tests or submission. Hold recorded and blocker mailed; real peers/byte teardown/wholeStage3 unverified. Evidence: test_logs/TEST_LOG_2026-10-26_19-07-50_client-reliability-9-resumed-ready.md.
|
||||
|
||||
- 2026-10-20: item6 DONE: retained actual profile-history wiring regression landed b3fab032;normal gate exit0 with431 discovered/431 unique file starts,focused38/38,5 killed mutants,lint11/11,submitted ancestry0. Test-only,existing product behavior passes. Evidence test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md. Real signers/relays/encryption/full-app modal/browser/build/synced Clear unverified. Expensive slot released;IDLE for owner local review,no optional work.
|
||||
|
||||
- 2026-10-20: item6 test-only retention ready:new real-profile wiring3/3,focused5files/38scenarios,5 isolated runtime mutants failnaturally/0cancelled,lint11pass. Local remove/lastitem/clear/recreation andfake-synced reduced+empty replacement covered;historicalno-op rejected. No product/existing suite edits;full/normal slot pendingexplicit grant,full/browser/build unrun;real signers/relays/fullapp/syncedClear unverified. Unique integrity/evidence in test_logs/*client-reliability-6*.
|
||||
|
||||
- 2026-10-20: item6 audit atd88716e3(profile paths unchanged throughbbf57388):real local profile remove/clear and fake-synced remove/recreation pass,0network;historical runtime no-op failsboth aftercleanup but existingprofile27/27 stillpasses;lowerlayers3files/8pass. Classify missing real-wiring regression,notcurrentproductfailure. Proposed test-only focused file+uniqueintegrity;await routing,QA expensive slot respected. Evidence test_logs/TEST_LOG_2026-10-20_15-04-00_client-reliability-6.md. No product/test changes or live signer/relay/fullapp proof.
|
||||
|
||||
- 2026-10-20: item5 done/closed after normal product landing0bd0b1c3(submission4bc52f9d,gate exit0,unstable ancestry exit0). Full427files/focused28/lifecycle18/six reverted mutants/lint11/build-artifact/Chromium+Firefox controlled smoke pass;obsolete mirror HEAD1→0. Browser+expensive slot released13:57UTC after10m57s initial wait. Dispatch landing/release event sent;preview refresh dispatch-owned. Closure metadata/final follow;no next item. Active abort/actual fallback/livepeers/CPU/fullStage3 unverified.
|
||||
|
||||
- 2026-10-20: item5 ready atmergedf2a392fa(product12f92732):full427/427 files60s-bound exit0,focused28/lifecycle18,six reverted mutants,lint11/build-artifact/native Chromium+Firefox smoke pass;obsolete mirror HEAD1→0,shared/rejoin/retry/hidden reveal pass,0 external/pageerrors. Full-slot wait10m57s recorded;browser released,expensive slot held through normal landing. Remaining queue/durable ancestry/close;active abort/real playback/CPU unverified.
|
||||
|
||||
- 2026-10-20: item5 routed controller-only pre-candidate guard implemented; baseline16pass/2fail→fixed18/18 lifecycle,6 rejected/reverted mutants,focused28/4files,lint11/build+artifact pass. No full/browser launch while dispatch administrative drain/QA reservation holds; slot requested13:32:54UTC,await explicit grant and verified unstable merge. No active abort/observer/cache/limits change; actual fallback/CPU unverified. Unique integrity/evidence in test_logs/*client-reliability-5*.
|
||||
|
||||
- 2026-10-20: item5 investigation at0c1dd7e8:active URL success/real4000ms controlled timeout release media/slot correctly;disposed active primary media error starts1 obsolete mirror HEAD(expected0),strict assertion naturally exits1 after cleanup/retry. Shared/rejoin owner and later same-source controls pass. Proposed controller-only pre-candidate ownership guard + focused tests; await dispatch routing. No product/tests changed; active abort/browser/fallback/CPU unverified. See test_logs/TEST_LOG_2026-10-20_13-30-00_client-reliability-5.md.
|
||||
|
||||
- 2026-10-20: item4 done/closed after normal landing89ccb907(submission7055496b,gate exit0,unstable ancestry exit0). Full427/427 files,focused22,lifecycle12,13 reverted mutants,lint11,build/artifact/native Chromium+Firefox controlled IO pass;obsolete starts1→0. Dispatch landing event sent; preview refresh owned by dispatch. No further item without routing. Actual media fallback/live peers/active abort/CPU and whole Stage3 unverified. Closure metadata/final event follow.
|
||||
|
||||
- 2026-10-20: item4 ready atce1381f5; full427/427 file exits0 with60s/file bound,focused22/22,lifecycle12/12,13 rejected/reverted mutants,lint11/11,build/artifact and Chromium+Firefox controlled native-observer smoke pass (obsolete starts0). Remaining normal queue/durable landing/ancestry then close/report. Actual fallback/live IO/active abort/CPU remain unverified.
|
||||
|
||||
- 2026-10-20: item4 routed implementation in URL observer/controller complete locally;12 lifecycle scenarios and13 reverted mutants, focused/lint/build pass. Queued last-owner work settles without cache/mirror/stale UI; shared owners, immediate retry and hidden reveal covered. Remaining: native browser smoke/full merged-tree gate/normal landing/closure; active abort/fallback/CPU unverified.
|
||||
|
||||
- 2026-10-20: client-reliability-4 investigation at4c54858c reproduces queued URL media start after feed disposal: active3/queued1 before and after disposal, then1 obsolete start(expected0); shared live-owner control deduplicates and receives healthy badge. Strict assertion exits1, diagnostic/control naturally exit0. No product/tests changed; awaiting dispatch fix routing. Active abort/browser/fallback/CPU unverified. Evidence: test_logs/TEST_LOG_2026-10-20_03-10-00_client-reliability-4.md.
|
||||
|
||||
- 2026-10-19: client-reliability-1 assigned: reproduce/classify and fix profile-modal suite hang without weakening assertions. Initial evidence: three isolated 60s timeouts (one clean environment), full baseline stopped at file 220/425; read docs/agents/prompts/client-reliability.md and claim the item. Scope excludes app.js and unrelated subsystems.
|
||||
|
||||
- 2026-10-19: client-reliability-1 fix committed at dbdac18d (test-only fixture, original assertions preserved; native focus contract and both mutants fail when broken). Isolated27/27 natural exit2.44s, lint11/11. Full60s/file run passed226/425 files including profile-modal; file227 tests/race/nostr-client-init-race.test.mjs timed out;198 later files untested. Await dispatch routing for this unowned blocker before normal landing/close. No product/browser/build qualification or landing claimed. See test_logs/TEST_LOG_2026-10-19_20-13-00_client-reliability-final.md.
|
||||
|
||||
- 2026-10-19: client-reliability-1 and client-reliability-2 ready for normal landing at181eae95: profile27/27 and race1/1 natural exit; all425 unit files passed with60s/file bound; lint11/11. Four deliberately broken variants fail and are reverted. Dispatch routing unblocked the second item; no product Nostr/shared-runner changes. Remaining: durable normal queue landing, unstable verification, then backlog close. Browser/build and real IO unverified (test-only scope). See test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md.
|
||||
|
||||
- 2026-10-19: client-reliability-1 and client-reliability-2 finished: durable normal-gate landing de6918ce (submission683cedf0), gate exit0 and unstable ancestry verified. Profile27/27, race1/1 natural exit, lint11/11, full425/425 file exits0; all four mutants rejected. Backlog closed after landing. No unfinished product work for these items; browser/build/real IO unverified (test-only scope). Closure metadata queued separately; see test_logs/TEST_LOG_2026-10-19_20-34-50_client-reliability-landed.md.
|
||||
|
||||
- 2026-10-19: client-reliability-3 implemented at current worktree: actual five-card obsolete starts1→0 after last-owner disposal; shared queued/live owners and active probe cache preserved; inflight identity and stale registration guards. Focused25/25 across4 files,11 mutants rejected, lint11/11, build/artifact verification pass, native Chromium/Firefox built-dist lifecycle smoke pass with0 external requests/page errors. Remaining: committed merged-tree full426-file gate, normal landq, durable ancestry/close and dispatch events. Active abort, URL probes, actual media fallback/peers and CPU/RSS unverified/out of scope; entire Stage3 performance plan is not complete.
|
||||
|
||||
- 2026-10-19: client-reliability-3 ready for normal queue at607d5ce1: full426/426 files exit0 with60s/file bound;27 focused scenarios,14 reverted mutants rejected, lint11/11, build/artifact verification pass. Corrected Chromium/Firefox built-dist/native-observer smoke:0 obsolete starts/stale callbacks/page errors/external requests; hidden reveal and source-health visibility pass. Remaining: normal queue/durable landing/ancestry then close and dispatch landing/final evidence events. Actual playback fallback/live peers, active abort/URL cancellation, CPU/RSS and full Stage3 remain unverified/out of scope.
|
||||
|
||||
- 2026-10-19: client-reliability-3 finished and closed after verified normal landing7d20a509 (submissione522efc1, gate exit0, unstable ancestry exit0). Full426/426 files,focused27/27,14 reverted mutants,lint11/11,build/artifact verification and native Chromium/Firefox controlled-IO smokes pass; obsolete starts1→0. Dispatch landing evidence sent; closure metadata/final report follow. Dispatch owns preview sync/rebuild after its resume; mailbox does not wake idle sessions. Actual media fallback/live peers/CPU-RSS, active abort/URL cancellation and whole Stage3 remain outside qualification; no new work without routing.
|
||||
|
||||
## storage-cors
|
||||
|
||||
- 2026-10-20 complete:productd3f4c215/closure7f99c045,full428/428,focused13/new9,10mutants,lint11/build;dispatch independent9/9+preview verification. Idle while QA trial output integrates. Live bucket/IAM/provider serialization/concurrent noCAS safety unverified.
|
||||
|
||||
|
||||
- Parallel trial item1: reproduce CORS existing-origin preservation offline before routing fix; no cloud changes.
|
||||
|
||||
## playback-qa
|
||||
|
||||
- 2026-10-20 item1 done:retained regression1bb3a4fc/closurebbf57388,full429/429/4focused/6mutants/lint11,dispatch independent4/4. Item2 nowactual-app local mock-relay/private-port core/mobile baseline,onebounded Chromium pass/noUIedits/browser-only allocation. Real peers/fullapp qualification stays open until actual evidence.
|
||||
|
||||
|
||||
- Parallel trial item1: local fallback qualification/regression; browser initially reserved to QA; product read-only.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bitvid scheduled-job policy
|
||||
|
||||
Owner direction, 2026-10-23: "I would like to not have extra processes like this running when the fleet is down or paused".
|
||||
|
||||
All seven installed Bitvid cron jobs enter through `tools/fleet/bin/bitvid-schedule-guard.mjs`, not directly through `sched.sh`.
|
||||
|
||||
- A missing/dead/stopped interactive fleet means no scheduled headless launch. At least one configured interactive area must have a running record and a live PID.
|
||||
- `fleet/PAUSED` is represented by the existing state-directory `PAUSED` marker. It unconditionally blocks every job, including the weekly restart job; `ignorePause` does not bypass this outer guard.
|
||||
- A running scheduled job is checked every second. On pause/down, the guard sends TERM only to the process group it created. Fleet's nested process runners forward termination to their own agent group. A bounded KILL fallback handles a group that does not exit.
|
||||
- A cron tick while down or paused starts only the brief guard check, which exits without an AI session or persistent watcher. There is no always-on supervisor added.
|
||||
- Unpausing and bringing the fleet up lets the next normal cron tick run; this policy does not automatically resume a paused fleet.
|
||||
- Existing quiet hours, per-job non-overlap locks and time limits still apply inside `sched.sh`. The one-heavy-job convention still applies.
|
||||
- This is a Bitvid-specific file, separate from the kit-managed scheduler. Kit upgrades should retain it. `fleet crontab` prints generic, unguarded commands: do not overwrite the installed guarded entries without preserving this policy.
|
||||
|
||||
Validation: `env -u NODE_TEST_CONTEXT node --test tools/fleet/test/bitvid-schedule-guard.test.mjs` passed 8 tests with natural exit; fixtures only, no real fleet shutdown/pause or headless model run. Test integrity: new behavioral coverage, no existing assertions/gates changed. Process stop tests use a disposable sleep child and verify the unrelated test parent remains alive. Nested production runner cleanup relies on the kit's existing TERM forwarding; this is not a real-model stop qualification.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Six-priority campaign — 2026-10-22
|
||||
|
||||
Owner: "yeah lets get the fleet spun up and address all of these", relayed by interpreter at14:02:34Z. Routine YES14:03:29Z. Prior holds superseded only within this foreground scope. Baseline unstable fb54cea3. Team is dispatch plus three existing workers; windows alive. Coordination c6022f21 landed c5b678f7 (durable status/log/ancestry checked). All three assignments acknowledged at c5b678f7: storage14:08:34Z, QA14:08:41Z, client14:08:44Z; each reports safe fast-forward and claim. No browser/expensive process started at acknowledgement; QA retains initial narrow slot. No current tool blocker reported.
|
||||
|
||||
| Priority | Accountable owner / item | Immediate action | Acceptance / boundary |
|
||||
|---|---|---|---|
|
||||
| 1 Firefox deterministic CI | playback-qa / playback-qa-3 | Inspect run39536282396/job119013565277 and local workflow/fixture; identify actual failing step/assertion, reproduce narrowly | Preserve first failure, exact commands/counts and natural exit; diagnose environment vs product. Root cause before fix/test/helper/config ownership. Strict retained regression + failing mutant for changes; targeted Firefox then unchanged required gates. Empty remote logs do not establish a cause. |
|
||||
| 2 Active cancellation | client-reliability / client-reliability-9 | Actual observer/controller HEAD last-owner disposal, shared owner, same-turn and later fresh rejoin with controlled fetch | Count abort/settlement/slots/cache/mirrors/stale writes; cancelled last-owner work releases promptly, shared live work continues, replacement identity safe, live success/error/timeout/cache/FIFO preserved. HLS and torrent separately measured before any completion claim. Investigation first; no product/test edits until routing and explicit contract note. |
|
||||
| 3 Playback reliability | dispatch / dispatch-4 (staged; client and QA next) | Audit publicly promised Range GET, persisted health/host scoring, retained warmup, hosted/P2P startup against current source and tests after cancellation slice | Range probe has bounded first chunk/cancel and HEAD-incompatible host fixture; persistence survives reload with bounded expiry/malformed/quota controls and source/host isolation; warmup is reused by playback and cancelled on owner loss with measured resource caps; hosted/P2P startup preserves configured preference, one winning playback and losing cleanup. Establish exact policy/ownership before writes, route real ambiguities. All four subitems open. Other four Oct playback followups remain tracked in QUALITY/canonical TODO. |
|
||||
| 4 Feed/view tombstones | storage-cors / storage-cors-3 | Actual delete/tombstone ingest then stale refresh using synthetic offline events/controlled relay boundary | Deleted video stays absent through cache refresh, late older event and view switching; unrelated live/newer valid content preserved under existing protocol semantics. Report real path/defect before edits; no new deletion-policy invention or real cloud/delete/signing. |
|
||||
| 5 Amber sessions | dispatch / dispatch-5 (staged; client next) | Re-audit per-account restore/switch/logout using existing NIP46 lifecycle tests and fake transport, read docs/nips first | Restore selects expected identity; switching and logout preserve other accounts; transient/denied restore handled without stale unknown identity or lost saved sessions. Pin real UI/service wiring where possible. No credential/live signer operation; real phone Amber remains explicit manual gate, not simulated proof. |
|
||||
| 6 Mobile | dispatch / dispatch-6 (staged; QA next) | Reproduce retained Upload overflow at360/390 plus1280 control; inspect actual app focus/touch and source | Strict document width <= viewport at360/390/1280; critical controls/touch/focus reachable, keyboard containment and restore retained, token/contrast rules followed. Locate culprit before CSS ownership/fix, preserve original failing baseline; real-device accessibility/manual check remains open. |
|
||||
|
||||
## Plan and sequencing
|
||||
|
||||
1. Inspect every worktree and preserve all dirty/unlanded work. Done: six clean, each HEAD-only count0. Dispatch ff-only current; workers sync own branches, never touch peers.
|
||||
2. Land coordination docs/backlog; route three disjoint investigations and collect explicit read/next-action acknowledgements.
|
||||
3. QA gets exclusive initial browser/expensive slot for narrow Firefox reproduction. Client/storage use cheap offline checks. No broad suite/build/normal product queue without dispatch grant; announce UTC start/release in .task and mailbox.
|
||||
4. Independently reproduce demonstrated defects, record expected contract/ownership and test-integrity requirements, then authorize smallest fixes. No app.js writer currently assigned.
|
||||
5. Stage remaining playback/Amber/mobile next as each owner frees up; do not close or omit them because another task completes. Add agents only for demonstrated disjoint work and permitted scope.
|
||||
6. Verify required focused/lint/mutants and unchanged normal gates; land via queue, verify durable status + unstable ancestry before closing. Integrated build/artifact/local preview and final six-row status, honest live/manual blockers.
|
||||
|
||||
No deploy/push/spend/production data/real-wallet/credential/live signer/cron authority. No retries, sleeps, force exits, weaker assertions or speculative fixes. No background monitoring claim. Read KNOWN_ISSUES before debugging; existing Firefox flakes and logout failure are baselines, not presumed causes of this CI failure.
|
||||
|
||||
## 14:12UTC evidence/routing checkpoint
|
||||
|
||||
QA released first browser14:11:17Z: original3 scenarios pass but app bootstrap failed because Buffer CDN was denied, so not CI reproduction. Next exclusive narrow Firefox diagnostic authorized only with pinned genuine cached Buffer and actual readiness, preserving assertions and isolated control; no broad/full/build.
|
||||
|
||||
Storage3 dispatch-independent scratch on7369f1fe/runtime-equivalentc5b678f7:7scenarios/3pass/4fail/network0, naturalexit1/29.89ms. Service snapshot/persistent cache leaks original deleted version; old tombstone unconditionally removes newer live active version in buffer; overlapped coordinator older result renders deleted reference at renderVideoList boundary. Two smallest service/buffer fixes routed in separate commits with mutants/controls; coordinator remains open/read-only pending faithful registered lifecycle evidence. No DOM/production proof.
|
||||
|
||||
## 14:23UTC QA checkpoint / mobile routing
|
||||
|
||||
QA3 native Playwright runner + original instrumentedTest/global setup/3callbacks/assertions PASS3/0fail/0flaky/0skip/retries0, workers1/private genuine Buffer/public-deny/scratch static server. Worker naturalexit0,9955ms,release around14:22:11UTC. Original CI184pass/2fail/3flaky remains OPEN/unreproduced; no speculative fix. App-ready3 and isolated3 and original-timing3 are distinct prior controls, not repeated CI proof. Access to failed CI logs403/no E2E artifact limits diagnosis; offline mirror/serial/serviceWorker setting/server differ. Retain QA3 evidence and stage further reproduction only on new evidence.
|
||||
|
||||
Priority6 now ACTIVE via QA4: actual-app Chromium360/390/1280 Upload width+culprit/focus/touch measurement, strict overflow baseline preserved, no CSS/product edits before independent evidence+routing. QA exclusive bounded browser, no full/build. Priority1 remains open; priorities2/4 fixes ongoing,3/5 staged.
|
||||
|
||||
## 14:31UTC gate and coordinator checkpoint
|
||||
|
||||
Storage3 existing buffer/service candidate owns full/normalgate/build slot since14:26:33, QA4 confirms mobile browser unconsumed/HOLD. Client9 exactcandidatee887ceaf independently23/23 naturalexit0/282.24ms and next fullgate, browser separate. Storage4 now separately routed coordinator-only after actual registered-feed/JSDOM2pass/2fail independent repro at89f8923c; freeze/submit storage3 first, keep follow-on separate. No product landing/CI clearance/live proof claimed.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Hourly check-in — 2026-10-24 19:22 UTC
|
||||
|
||||
- Read PROGRAM, WIND-DOWN, current RULE-CHANGES, repo memory, active backlog,
|
||||
task lines, dispatch mailbox (empty), and landing results/log.
|
||||
- `node tools/fleet/bin/doctor.mjs --json`: exit 0 at 19:22:16Z. Dispatch DIRTY4,
|
||||
client ONE-DISK2, storage clean, QA DIRTY3 plus one WAITING line. Existing work
|
||||
was preserved. Doctor also reports unowned paths; ownership was not changed.
|
||||
- Every WAITING line read: QA waits on dispatch since 03:00:28Z, over 16 hours.
|
||||
Sent dispatch one scheduler mailbox notice at 19:23:03Z asking acknowledgement
|
||||
and resolution or explicit parking. Recorded delivery in today's sent ledger;
|
||||
retained the heavy HOLD and granted no execution slot. Consumption unverified.
|
||||
- `node tools/fleet/bin/landq.mjs status`: exit 0, queue 0, lander NOT running.
|
||||
Appended dated owner line; an empty-queue drain normally exits by design.
|
||||
- Sole failed result `d7184f07bd` is publication divergence, already known to
|
||||
submitter and recovered per fleet/LOG.md. Append-only log also preserves two
|
||||
red gates overwritten by later same-SHA success results. Added these to
|
||||
STRIKES with environment/thermal context, one per area this week. No tier
|
||||
move threshold reached. No outstanding conflict found in retained records.
|
||||
- Usage governor: `usage.enabled=false`, `modules.usageGovernor=false`;
|
||||
configured wind-down threshold 95%. Adapter usage hook was not called;
|
||||
usage-based PAUSED step does not apply.
|
||||
- No product edits, tests, builds, browser runs, push, deploy, approvals,
|
||||
scheduler installation or interactive session starts. Main/product health,
|
||||
usage percentage, live playback, owner acknowledgement and message consumption
|
||||
were not verified. Documentation landing is checked separately after submit.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Hourly check-in 2026-10-26 19:22 UTC
|
||||
|
||||
- Scope: fleet health, all WAITING text, landing results, daily notification deduplication, strikes and usage wind-down. No product changes or heavy validation.
|
||||
- Plan completed: read PROGRAM/WIND-DOWN/backlog/STRIKES and daily ledger; run doctor/queue/fleet status; inspect every area .task including inline WAITING; review retained adverse landing log; record new strike and owner decision; submit only this audit and STRIKES.
|
||||
- doctor --json, landq status, fleet status, landq status c20635bc, backlog list: exit0. Doctor: dispatch DIRTY21/WAITING, client ONE-DISK9, QA DIRTY10, storage behind22. Existing work retained.
|
||||
- All .task files read. Dispatch thermal WAITING since19:16Z and inline client WAITING since19:15:42Z are under2h. No overdue reminder. Storage .task still mentions its historical queued landing; durable result492557400c is landed, not a fresh blockage.
|
||||
- Fleet reports lander DOWN, queue0; workers unavailable/dead, storage stopped with1 unread, interpreter3 unread. No status says stalled; a dead/unknown window proves neither progress nor a live stall. Lander absence recorded in owner.md.
|
||||
- Retained log adds one new red c20635bc at19:15:32.782Z; durable result rejected. Existing client MCP notice19:16:19Z is in the daily ledger, so no duplicate send or ledger append. No additional conflict/withdrawal found in retained landing log. Session-only incident completeness unverified.
|
||||
- STRIKES records thermal interruption without claiming assertion failure. Two client reds within7days straddle configured Monday03:00 usage reset: current configured week1. Owner proposal conditional on rolling-week interpretation; no tier/config edit.
|
||||
- usage.enabled=false and modules.usageGovernor=false; no usage-hook query or PAUSED creation. Latest40 wake records contain no five-hour-nearly-gone warning; panes not scraped.
|
||||
- No lint/unit/build/browser rerun for Markdown-only audit during thermal hold. No product/main/live qualification, real session liveness, worker acknowledgement or hold-compliance verification.
|
||||
- Verification: git diff --check limited to audit files before commit; docs-only landing result and target ancestry checked separately after submission.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Local review handoff — 2026-10-20
|
||||
|
||||
Status: ready for owner local review with the known minor Upload overflow below. Owner subsequently authorized bounded active-probe/URL cancellation work;the active URL-media slice is now landed43cb1baf. New localpreview build08620a71 replaces prior6da89895:build/artifact exit0,hostHTTP200/indexbyteequalcmp0. Dispatch independently29/29 lifecycle;unchanged normal432-filegateexit0. Worker native Chromium+Firefox controlled media smoke passed and owned resources released. These use actual controller/observer/native src/load plus controlled state/HEAD/intercepted media IO and synthetic completion;pause is stubbed. Not full-app decoder/live playback or universal networkabort/byte/CPU proof. HEAD/HLS/torrent cancellation remains open. Closure/browser evidence durably2ea6f068;item8 done,team idle until next bounded scope.
|
||||
|
||||
Open [the existing local preview](http://127.0.0.1:3000). It is already running; no terminal command is needed. The existing dispatch preview serves built `dist`. Current build08620a71 is from integrated43cb1baf. Historical core/mobile QA build6da89895 (an opaque artifact identifier, not a Git SHA) was produced from integrated unstableb1174163, including the cold-feed fix. HTTP200 and byte equality of served and built index were verified; runtime equality for final documentation descendants will be checked before handoff. Dispatch branch is area/dispatch. Final documentation landing SHA is reported in the owner handoff; runtime/build-input equality through QA closurec017069c was verified without rebuilding.
|
||||
|
||||
## Changes available to review
|
||||
|
||||
- Disposed feed owners no longer start obsolete queued torrent/URL media probes; an active failed media probe also cannot start an orphan mirror HEAD. Shared owners and active work retain existing semantics.
|
||||
- Fresh relay video updates now notify the current feed consumer, so a cold empty feed can render newly arrived eligible videos. Obsolete subscription callbacks cannot update a replacement feed.
|
||||
- Bucket CORS updates preserve existing rules and append only missing origin coverage. Unexpected reads cannot overwrite policy.
|
||||
- Storage cleanup safely skips malformed cached primitive fields rather than rejecting; R2 and generic S3 deletion share this service. Ownership boundaries remain enforced.
|
||||
- Actual playback fallback/magnet behavior and actual profile-history removal wiring now have regression coverage. The history behavior already worked; the new test catches the historical no-op implementation.
|
||||
|
||||
## Evidence
|
||||
|
||||
Latest unchanged normal gate candidate b1174163 completed successfully across432 configured test files, with empty quarantine and no shard. Durable landing and submitted ancestry were checked independently. Retained runner output contains432 unique starts; the stored gate result is a tail, not a complete install/process transcript, and its final stdout marker is absent. No suite or gate was weakened.
|
||||
|
||||
Dispatch independently checked URL lifecycle18/18, CORS9/9, fallback4/4, malformed cleanup13/13, new real history wiring3/3, and cold-feed current-consumer notification7/7. Workers also recorded failing restored mutants, focused suites, lint11 stages, and normal full gates. [History evidence](../../test_logs/TEST_LOG_2026-10-20_15-41-00_client-reliability-6-landed.md) and [dispatch evidence](../../test_logs/TEST_LOG_2026-10-20_15-45-00_dispatch-review.md) retain exact scope.
|
||||
|
||||
Before the fix, the actual-app reproduction was: cold Trending started from an empty cache, received three valid eligible local video events, and retained an empty DOM. Passive trace proves relay EVENT/EOSE, all client/app maps and filtered videos contain3, while view remainsEMPTY. Client7 fix landedb1174163 with active-consumer and stale-callback lifecycle controls plus regression coverage. The rebuilt artifact6da89895 renders all three cold-feed cards without a forced refresh. The controlled browser uses genuine cached Buffer and a genuine local VP8 fixture outside immutable app files, synthetic identity/notes and local mock relays. This does not qualify real signers, public relays or deployed CDN behavior.
|
||||
|
||||
## Actual-app baseline and known finding
|
||||
|
||||
Controlled Chromium154 on immutable build6da89895 completed Trending/Explore/ForYou/Home navigation, three-card hydration, synthetic login, Profile and Upload open/close, six Tab steps contained in each modal, exact trigger focus restoration, missing-title and missing-source validation, and no video publish from invalid forms at360×800,390×844 and1280×720. Feed cards/document fit those viewports. Genuine cached Buffer, real toolkit signature checks and local VP8 were used; public HTTP/WebSocket traffic was denied, with no page errors or local asset failures in these final runs.
|
||||
|
||||
The original strict baseline stopped on Upload horizontal document overflow. It remains FAILED/open: document width362 at360px and394 at390px; no overflow at1280. Targeted follow-on coverage completed independently; critical inputs, Publish and Close remain horizontally inside the viewport and close restores focus. This is not a wholly passing baseline. Exact overflowing source element is not yet established; no speculative CSS fix was made. Some measured controls are below44px; real-device usability/accessibility review remains open.
|
||||
|
||||
The browser, private app/relay servers and temporary dependency link were closed after the follow-on run at16:28:10.112UTC. The owner preview on3000 remains running.
|
||||
|
||||
See [QA evidence and screenshots](../testing/playback-qa-2-final-evidence.md) for exact final and follow-on results, preserved failures and fixture provenance.
|
||||
|
||||
## Suggested review in the browser
|
||||
|
||||
1. At desktop and narrow mobile widths, browse Trending/Explore/For You, open and close Profile and Upload, and check keyboard focus and horizontal scrolling.
|
||||
2. In Upload external mode, check missing title and missing playable source errors. Avoid publishing or using real bucket credentials during this local review.
|
||||
3. Check playback source/fallback messaging with content you intend to test. Controlled fallback tests do not establish live torrent availability.
|
||||
4. Check history removal/clear and reload behavior using the account/signing workflow you intend to qualify; real account evidence is still open.
|
||||
|
||||
## Remaining scope
|
||||
|
||||
Actual full-app media playback/Firefox, real Amber/NIP46 account/switch/logout, encrypted relay persistence, live storage/IAM/provider behavior and concurrent CORS writes, real WebTorrent peers, production service-worker/offline/CDN behavior, real devices and manual release QA remain unverified. Known parallel Firefox hydration and isolated logout issues remain documented in KNOWN_ISSUES.md.
|
||||
|
||||
Feed/view tombstone audit remains deferred as storage-cors-3. Public-promised playback backlog remains open: RangeGET health compatibility, persistence/per-host scoring, parallel startup, hover/viewport warmup, complete mirror/Blossom discovery, ranking/status and service-worker followups. Partial existing implementations are not whole-item completion. See [quality matrix](../../fleet/QUALITY.md).
|
||||
|
||||
This milestone is local review/testing, not production readiness or completion of the broader roadmap. Workers idle after evidence closure; no push, deploy, spend, real cloud/account/peer operation or scheduler activation was performed.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Program — the owner's priorities and rulings
|
||||
|
||||
Last updated: 2026-10-23
|
||||
|
||||
## Current milestone
|
||||
|
||||
Six-priority campaign resumed 2026-10-22; prior completion statements below are historical evidence. See CAMPAIGN-2026-10-22.md.
|
||||
|
||||
Active URL-media cancellation slice is landed43cb1baf after normal432-file gate exit0;dispatch independent29/29,build08620a71/artifact pass/previewHTTP200+index equality. Worker controlled native Chromium+Firefox smoke passed and resources released. Closure/browser evidence durably landed2ea6f068;item8 done,team idle awaiting next bounded scope. HEAD/HLS/torrent active cancellation remains open; broader optional work remains deferred.
|
||||
|
||||
Local-review handoff: storage2/history6 and demonstrated cold-feed blockerclient7 are normally landed. Build6da89895/preview3000 verified;actualcold3cards and separate25-step360/390/1280core/modal coverage complete. Original strictUploadoverflow FAIL remains open(2/4px,controlsusable),not an all-green baseline. Finish docs-only evidence/handoff,then allIDLE for owner review;no optional backlog work. Broader roadmap/live signer/cloud/peer/release gates remain explicit in QUALITY/LOCAL-REVIEW.
|
||||
|
||||
## Priorities
|
||||
|
||||
1. Owner direction: TODO backlog progress, bug fixes, robustness, and usability.
|
||||
2. Session-manager sequencing (vetoable): prioritize reproducible failures in core user flows and small changes with clear completion checks.
|
||||
|
||||
## Rulings log (newest first)
|
||||
|
||||
## 2026-10-23 — Campaign continuation after resource interruption
|
||||
|
||||
> "ok lets get the fleet working some more" — owner, relayed by interpreter at2026-10-23T12:48:12Z; not typed in dispatch.
|
||||
|
||||
Continue six existing priorities, with client HLS exact-candidate prep/normal gate when safe, QA bind-permission diagnosis/host-launch prep, and disjoint storage read-only playback audit. Preserve all rejected/partial evidence. Interpreter freezes direct/setup target writes during product gates and requires actual dispatch exclusive-slot acknowledgement before heavy validation; queue snapshots do not reserve a slot. No new restricted-action authority.
|
||||
|
||||
## 2026-10-23 — Scheduled processes must respect down and pause
|
||||
|
||||
> "I would like to not have extra processes like this running when the fleet is down or paused" — owner, relayed by interpreter at2026-10-23T12:35:35Z; not typed in dispatch.
|
||||
|
||||
Interpreter is implementing a local Bitvid schedule guard and focused tests in unique files. Intended scope: all seven existing schedules require a live interactive fleet run; PAUSED blocks every job including weekly restart, and a supervised scheduled child is terminated only within its owned process group when fleet pauses/goes down. At12:37:56Z interpreter reports guard installed for all seven entries and8focused fixture tests passing naturally. Dispatch inspected guard/policy source at printed unstable3a89abd0; installed crontab execution and real-model shutdown remain unverified. No always-on watcher; inactive cron ticks run only the brief guard. Existing HLS gate/landings remain untouched; no unrelated process stops, global kit change or restricted authority added.
|
||||
|
||||
## 2026-10-23 — Existing seven schedules authorized in interpreter session
|
||||
|
||||
> "All seven jobs shown above" — owner, relayed by interpreter at2026-10-23T12:32:43Z; not typed in dispatch.
|
||||
|
||||
Interpreter reports installing the existing checkin, hygiene, triage, summary, winddown, restart and resources jobs in user crontab, preserving prior entries and verifying readback. Scoped PATH/FLEET_CONFIG; resources every15minutes, only while laptop awake. Installation/readback are interpreter evidence, not independently verified here. This records scope and provenance; dispatch did not install or alter cron. One-heavy-job/resource guidance and all restricted-action limits remain. It adds no push/deploy/spend/delete/credential authority. Interpreter separately reports owner-approved MCP-only autoapproval setting saved; effective state in existing sessions is unverified and does not override a denied action.
|
||||
|
||||
## 2026-10-22 — Six-priority campaign resumed via interpreter
|
||||
|
||||
> "yeah lets get the fleet spun up and address all of these" — owner, relayed by interpreter at 2026-10-22T14:02:34Z; not typed in dispatch.
|
||||
|
||||
The relay identifies six priorities: Firefox deterministic CI failure; remaining active HEAD/HLS/torrent cancellation; playback reliability promises; feed/view tombstone stale-refresh; Amber sessions; mobile usability. This supersedes the prior idle/local-review and quiet-hours hold for this foreground campaign. Routine investigation, evidence-routed fixes, tests/builds and normal local unstable landing are approved. No push/deploy/spend/production data/real wallet/credential/live signer/scheduler authority. Track all six in docs/agents/CAMPAIGN-2026-10-22.md. Preserve worktrees, use three existing workers with disjoint scope, stage before fixes, serialize browser/full gates, keep app.js single-writer. Final integrated local build/preview and honest live/manual limits required.
|
||||
|
||||
### 2026-10-20 — Resume bounded active-probe cancellation, relayed by interpreter
|
||||
|
||||
> "Then continue with the active-probe/URL cancellation work." — owner, relayed by interpreter at20:44:41UTC; not represented as typed in dispatch.
|
||||
|
||||
Route existing client worker to client-reliability-8: reproduce current active URL-media last-owner disposal through actual observer/controller entrypoints and controlled IO, with shared live-owner and fresh-rejoin controls. Measure resources, settlement, slot release, cache and stale callbacks before any fix. Start media before broader HEAD/HLS/torrent policy. Existing queued/orphan-mirror fixes are complete; active abort is not. Existing tests protect the previous queued-only contract; expectation changes require explicit routing and Test Integrity Note after evidence. No app.js/kit/scheduler/external/cloud/signer/push/deploy/spend authority. Other workers idle unless needed for bounded independent QA.
|
||||
|
||||
### 2026-10-20 — Explicit local-review stopping milestone,relayed by interpreter
|
||||
|
||||
> "Ready for me to review and test locally" — owner, relayed by interpreter;not represented as typed in dispatch.
|
||||
|
||||
Finish current storage2/history6 gates/landings,faithful local actual-app/core/mobile baseline and preview refresh,then provide local-review handoff before declaring this milestone met. Do not clear optional roadmap or claim production/live Amber/cloud/peer proof. No new restricted-action authority.
|
||||
|
||||
|
||||
### 2026-10-20 — Continuing quality objective, relayed by interpreter
|
||||
|
||||
> "ok try to keep them working until the project is in a good place." — owner, relayed by interpreter; not represented as typed in dispatch.
|
||||
|
||||
Continue the existing robustness/usability backlog beyond the trial, with bounded evidence-first tasks and adaptive staffing. Local proof and real Amber/cloud/live-peer/manual release gates stay distinct. This does not add push/deploy/spend/real-wallet/production-data/scheduler authority or authorize new product definitions. Track current quality and next work in fleet/QUALITY.md.
|
||||
|
||||
|
||||
### 2026-10-20 — Bounded parallel trial, owner words relayed by interpreter
|
||||
|
||||
> "id really like to stress test the multi agent process so if it needs more agents to get more done faster. do it" — owner, relayed by interpreter; not represented as typed by owner in dispatch.
|
||||
|
||||
Interpreter requests executing dispatch plus up to3 workers now with disjoint worktrees/scopes. Routine launches are authorized; retain existing restricted-action limits. Trial measures acknowledged assignments, actual parallel evidence/checks, durable queued landings and contention, not process count.
|
||||
|
||||
|
||||
### 2026-10-19 — Roster-only bootstrap gate (session manager call, VETOABLE)
|
||||
|
||||
The first approved worker is assigned to the pre-existing profile-modal test hang. Normal roster-before-launch ordering otherwise invokes that same hanging full-suite gate before a worker can investigate. For this one administrative roster/brief commit only, dispatch will use a temporary queue configuration whose gate verifies the exact committed change contains only the expected area registration and documentation/backlog metadata, with product code, tests, and persistent gates unchanged. No product fix is covered by this exception. Preserve normal full gates for subsequent code. Record the actual gate result and disclose the unresolved unit failure.
|
||||
|
||||
### 2026-10-19 — Routine fleet authority, relayed from Codex desktop chat
|
||||
|
||||
> "yes please approve whatever it wants and let me know about it" — owner, as relayed in this session from the Codex desktop chat; not represented as typed by the human in this terminal.
|
||||
|
||||
The relay instruction received here explicitly authorizes operating tmux as the fleet interface, approving and starting the proposed client-reliability worker, and making routine staffing, local test/build, backlog prioritization, fixes, and local unstable landing decisions within the recorded goal without repeated approval. Keep the team small and scopes separate. This does not authorize deploys, spending, production data, real-wallet operations, or scheduler activation. Refresh the existing local preview after verified changes; send concise owner-mailbox updates for launches, assignment changes, landings, and blockers, including team size, areas, tasks, and verification. This supersedes the initial waiting state for first-worker approval only within that routine-work scope.
|
||||
|
||||
### 2026-10-19 — Initial goal
|
||||
> "lets try to work our way through some of the issues in the TODO backlog and find and bugs and generaly make the client more robust and usable. ." — owner
|
||||
Applies to: all areas. Recorded by: dispatch.
|
||||
|
||||
### 2026-10-19 — Conservative startup limits (session manager call, VETOABLE)
|
||||
|
||||
No deploys, spending, production data, real-wallet operations, or scheduler activation. Present the first team for owner approval before starting workers.
|
||||
|
||||
## Standing rules
|
||||
|
||||
- Approvals for spend, publishing and deploys are typed by the owner in the session that acts.
|
||||
- Agreed work is never silently dropped: a priority change defers it; only the owner cancels, with a reason.
|
||||
Loaded 100 of 590 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user