Ctrl K
ComunidadGuías…

docs/PAGES.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Archiver on GitHub Pages — the public site

## What 5.5 changed for Pages

- **Scope is `/Archiver/`.** Every URL the page or service worker uses is
  relative to the project-pages root, and `tests/pages.js` asserts there are no
  root-absolute `/static` or `/api` references in the page, engine or
  controller.
- **No root `/api/` calls.** Server calls go through the page's `api()` helper,
  which rebases through `ABS()` and is disabled when the server is not
  reachable. On Pages the server is unreachable by design, so no call is made.
- **Shell cache `archiver-shell-v5.5.0`.** The service worker deletes only its
  own older `archiver-shell-*` caches. Other origins' caches and the model
  cache are not touched.
- **Answer cache is tied to the grounding policy** (`grounding-5.5`). An answer
  stored under an older policy is regenerated, not replayed.
- **Cross-origin isolation bootstrap is kept.** The one-time service-worker
  bootstrap still supplies the COOP/COEP headers that Pages cannot send.
- **Not verified in this release:** a reload-loop test. The bootstrap is
  unchanged, but no test was added for it in 5.5.


**Public site:** <https://2archiver.github.io/Archiver/>
**Workflow:** `.github/workflows/pages.yml` → `scripts/build_pages.py` → `actions/deploy-pages`

The public-facing app has moved to the project site on GitHub Pages. The Render deployment remains available as an optional backend for live web search and cross-device sync; GitHub Pages is the public front door and runs the local-first client. Pages hosts the *frontend only*. That is the whole point of the architecture:
generation runs in the visitor's browser (WebGPU where the browser has it, the
WebAssembly runtime where it does not), the knowledge corpus is a bundled
JavaScript file, and conversations live in `localStorage`. Nothing server-side
is deployed, so there is no database, no memory bank and no API key that could
leak — the builder additionally refuses to emit `*.db`, `*.py`, `.env`, `.git`
or key files even if they existed in the checkout.

## The Pages source: GitHub Actions, asserted by the workflow

Pages has to build from the workflow rather than from a branch folder, and the
workflow no longer assumes that is already true.
`scripts/ensure_pages_source.py` runs before every build on `main` and is a
no-op unless something is wrong:

- source already *GitHub Actions* → one GET, nothing else;
- source *Deploy from a branch* → `PUT /repos/…/pages` with
  `build_type=workflow`, then a re-read to confirm it took;
- Pages not enabled at all → the same call as a `POST`.

If the API refuses (the run's token may not change repository settings), the
step fails with the settings path instead of deploying into a host that will
404. The manual equivalent remains

```bash
gh api -X PUT repos/2archiver/Archiver/pages -f build_type=workflow
```

or **Settings → Pages → Build and deployment → Source → GitHub Actions**.

`actions/configure-pages@v5` with `enablement: true` looks like the standard
way to assert this and is not: it calls `getPagesSite()` first and returns the
existing site when there is one, so `build_type: workflow` is only sent when
Pages is off entirely. Against a branch-sourced site it succeeds in about a
second having changed nothing, which is why the API call lives in the repo.

## A green deploy is not a working site (2026-10-02)

On 2026-10-02 <https://2archiver.github.io/Archiver/> served GitHub's 404 page
— *"The site configured at this address does not contain the requested file …
For root URLs (like `http://example.com/`) you must provide an `index.html`
file"* — while the `Pages` workflow's deploy job and the `github-pages`
deployment status were both green.

What had happened: the repository's Pages source was on *Deploy from a branch*
(`main` / `docs`), so GitHub's built-in builder ran on the same push as the
workflow, landed a couple of minutes later, and replaced the deployed app with
its own build of `docs/` — five rendered markdown files and no `index.html`.
The site's last successful deployment was that 34 KB artifact, while the
workflow's was 9.4 MB. `/Archiver/` 404'd; `/Archiver/PAGES.html` answered 200
with the rendered copy of this file, which is how the leftover was identified.
`actions/deploy-pages` had done its job — it uploads an artifact and asks Pages
to publish it, and nothing tells it that something else publishes afterwards.

Three guards now make that failure hard to repeat and impossible to miss:

- **The source assertion above**, so GitHub's built-in builder stops being
  scheduled at all.
- **A settle step**: when the assertion did change the source, a built-in build
  already queued for the same commit is allowed to finish before the deploy, so
  the workflow's artifact is the last one published (a settings change stops
  new builds being queued, but not a run that already exists).
- **A post-deploy check**: `scripts/verify_pages_site.py` fetches the URL
  `actions/deploy-pages` reported and requires the app shell (the page must name
  `static/archiver-engine.js` — GitHub's 404 page and the docs-only build do
  not) plus a 200 for `manifest.json`, `favicon.svg`, `archiver-coi-sw.js`,
  `static/archiver-engine.js`, `static/archiver-worker.js` and
  `static/vendor/wllama-3.6.1.wasm`. It retries for two minutes because a fresh
  deployment takes a moment to reach every edge, and it fails the run with the
  settings path if the host is serving something else.

The workflow also runs the whole thing once a day (`cron: '17 6 * * *'`), so a
site replaced by a hand-made settings change is repaired — or reported red —
within a day rather than waiting for the next push.

## How one codebase serves two hosts

Every asset reference in `web/` is document-relative and every same-origin
fetch is rebased through a small `ABS()` helper:

- Render (uvicorn/`app.main`): page at `/` → `static/…` → `/static/…`, `api/…` → `/api/…`.
  Byte-for-byte the same URLs as before.
- Pages: page at `/Archiver/` → the same refs resolve under `/Archiver/…`.

The `web/manifest.json` PWA fields (`start_url`, `scope`, icons) are relative
too, so an install from Pages is scoped to `/Archiver/` instead of claiming
the whole `github.io` origin.

## What static mode disables

After the local interface and cached conversation are ready, the app probes
`api/health`. A static host answers 404, the app marks `SERVER.online = false`,
and from then on:

- **Server sync** (session mirroring, settings store) is skipped silently —
  chats still save and reload from this browser.
- **Live web search** needs the optional backend and cannot retrieve sources on
  Pages; local knowledge and on-device generation remain available.
- **Settings → Export archive** says it needs the server and points at the
  per-conversation Download, which is browser-local.
- There is no startup warning toast; the local app opens immediately. Actions
  that specifically need the server explain that when attempted.

## Shared memory without COOP/COEP headers

The vendored `wllama` build allocates `WebAssembly.Memory({shared:true})`
even to run one thread, and browsers only grant shared memory in a
cross-origin-isolated document (COOP + COEP). The app server sends those
headers itself; **GitHub Pages cannot send any response headers.**

`scripts/build_pages.py` therefore injects a five-line bootstrap that
registers `web/archiver-coi-sw.js` — a service worker that synthesizes
`Cross-Origin-Opener-Policy: same-origin` and
`Cross-Origin-Embedder-Policy: require-corp` on same-origin **documents and
worker scripts**. That is the standard GitHub-Pages trick for isolation; the
page reloads exactly once (guarded by `sessionStorage`, no reload loops) and
lands isolated. Desktop Chrome and Edge gate `SharedArrayBuffer` behind
isolation as well, so every desktop browser takes that one reload on its first
visit; a page that is already isolated never reloads. If a browser refuses
SW-granted isolation, the engine's preflight fails before the weights download
with a message that names the cause instead of a mystery.

**Worker scripts matter.** A page served with `COEP: require-corp` refuses to
start a worker whose script response does not carry COEP itself. Stamping only
navigations (the first version of this worker) left the WebGPU model worker
(`static/archiver-worker.js`) blocked on every isolated page — Chromium reports
`net::ERR_BLOCKED_BY_RESPONSE`. Opaque and redirect responses are handed back
untouched, since they cannot be rebuilt. `tests/pages.js` runs the worker
against a fake scope to keep all of this honest.

This mirrors Render's own `ARCHIVER_COEP=require-corp` default, so the set of
subresources (same-origin files, CORS-mode model downloads from
huggingface.co) is unchanged — everything the Pages site loads already
satisfies `require-corp`.

## The model is not what limits github.io

Pages only hosts the page and the two vendored runtimes (`web/vendor/`, about
20 MB). Model weights are never in the repository or the artifact: both
families are fetched from huggingface.co in CORS mode (which `require-corp`
allows) and cached in the visitor's browser, so *which* model is the default
changes the visitor's download and GPU memory, not whether the site deploys.
What did gate the site was the host: server-rooted URLs, no backend, and no
way to send COOP/COEP headers — the sections above. The default is
Qwen2.5-0.5B-Instruct with Qwen3-0.6B as fallback
(see [MODEL-UPGRADE.md](MODEL-UPGRADE.md)); on the CPU path both are the same
~429 MB download.

## What was verified, and what was not

Checked in headless Chromium 153 against the built site served at `/Archiver/`
by a static server that sends no COOP/COEP headers (a stand-in for Pages on
`http://localhost`):

- the service worker registers, the page reloads exactly once, and
  `crossOriginIsolated` / `SharedArrayBuffer` are then available (neither is
  available in desktop Chromium before that);
- the WebGPU model worker (`static/archiver-worker.js`, a module worker) is
  blocked with the first version of the service worker and starts with this one;
- with a small random-weight GGUF standing in for the Qwen2.5 file (served from
  a mocked `huggingface.co`), the unmodified page loads the real vendored
  wllama 3.6.1 build, starts its multithreaded workers, requests the
  `qwen2.5-0.5b-instruct-q4_0.gguf` URL first, and streams a generation;
- no console errors or failed requests other than the expected `api/health`
  404 of the static-mode probe.

**Not verified:** WebGPU generation (headless Chromium has no usable adapter),
real Qwen weights or answer quality, Hugging Face reachability from a visitor's
network, and Safari or Firefox. Treat those as the first things to try on the
live site.

## Rebuilding locally

```bash
make pages                      # → dist/pages/
python3 -m http.server -d "$(dirname dist/pages)" 8080   # then http://localhost:8080/pages/
make test                       # includes tests/pages.js and tests/test_pages_deploy.py
```

`tests/test_pages_deploy.py` covers the two guards offline: the source assertion
against a fake `gh` (already-workflow, branch-sourced, not-enabled, refused and
write-did-not-take), and the post-deploy verifier against a real loopback HTTP
server serving an app-shaped site, a docs-only site (the 2026-10-02 state) and a
site missing an asset. One test builds the real site and checks every path the
verifier demands still exists, so a rename cannot turn a good deploy red.

## What remains Render-only

The API, web-search proxy, settings/archive export, and cross-device session
sync. Nothing in this file changes those — `app/main.py` gained no routes and
the injected bootstrap lives only in the built Pages artifact.