Spaces:
Running
Running
Eric Putnam
Claude Opus 5 (1M context)
feat: deploy simpledocreview as a browser-only static space
3d968d9 |
Download DEPLOY.md from Hoctar77/SimpleDocReview: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/DEPLOY.md
- Command line
-
hf download hf://spaces/Hoctar77/SimpleDocReview/DEPLOY.md
-
curl -L -o DEPLOY.md https://huggingface.co/spaces/Hoctar77/SimpleDocReview/resolve/main/DEPLOY.md
11.7 kB
| # Deploying to Hugging Face | |
| This repo deploys [SimpleDocReview](https://github.com/eputnam77/SimpleDocReview) | |
| to a Hugging Face **Static** Space as a browser-only demo. It holds no | |
| application logic of its own β only the build tooling, the Space card, a vendored | |
| copy of upstream in `app/`, the browser-side API in `overlay/`, and the generated | |
| site at the repository root. | |
| ## Why static | |
| Per Hugging Face's own docs, [Static Spaces][static-docs] are free for everyone β | |
| "served directly without running on compute, so unlike Gradio and Docker Spaces, | |
| creating them never requires a paid plan." | |
| Upstream is a good fit in one respect and a poor fit in another, and both matter: | |
| - **Good fit:** the front end is plain HTML, CSS, and JavaScript with no build | |
| step, no framework, and no bundler. It is served byte-for-byte as upstream | |
| wrote it. | |
| - **Poor fit:** upstream is a *server*. `app.py` owns the review logic and the | |
| storage. A Static Space cannot run it, so the API is reimplemented in the | |
| browser and the storage becomes IndexedDB. | |
| That second point is the whole design constraint of this repository, and it is | |
| why the Space is a demo. See [What the demo cannot be](#what-the-demo-cannot-be). | |
| [static-docs]: https://huggingface.co/docs/hub/spaces-sdks-static | |
| ## The everyday workflow | |
| After you change something in `C:\dev\SimpleDocReview` and commit it there: | |
| ```powershell | |
| cd C:\dev\sdr-streamlit | |
| .\scripts\deploy.ps1 | |
| ``` | |
| That syncs `app/`, builds the site into the repository root, commits with the | |
| upstream commit in the message, and pushes to `hf`. Because a Static Space serves | |
| files directly, the update is live as soon as the push lands. | |
| To see what would change without committing: | |
| ```powershell | |
| .\scripts\deploy.ps1 -DryRun | |
| ``` | |
| Individual steps, when you want them: | |
| ```powershell | |
| .\scripts\sync-upstream.ps1 # refresh app/ from the local upstream checkout | |
| .\scripts\build-static.ps1 # build the site into the repository root | |
| node tests\parity\run-parity.mjs # diff the browser API against the real app.py | |
| node tests\browser\run-browser-check.mjs # drive the built site in headless Chromium | |
| python -m http.server 8000 # preview at http://localhost:8000 | |
| ``` | |
| ### Useful flags | |
| | Flag | Effect | | |
| | --- | --- | | |
| | `-Message "feat: ..."` | Custom commit message | | |
| | `-AlsoPushOrigin` | Also push to the GitHub mirror of this repo | | |
| | `-UpstreamPath D:\other\checkout` | Sync from a different checkout | | |
| | `-AllowDirty` | Deploy from an uncommitted upstream working tree | | |
| | `-SkipSync` | Build and deploy `app/` as it stands, no re-mirror | | |
| | `-SkipBuild` | Commit and push the existing site, no rebuild | | |
| | `-SkipTests` | Skip the parity and browser checks (they run by default) | | |
| | `-DryRun` | Show the plan, change nothing | | |
| By default a dirty upstream working tree is an **error**, because `UPSTREAM.json` | |
| would otherwise record a commit that does not describe what actually shipped. | |
| Commit upstream first, or opt in with `-AllowDirty`. | |
| ## First-time setup | |
| 1. Create the Space at https://huggingface.co/new-space β choose the **Static** | |
| SDK and the **Blank** template. Name it `SimpleDocReview`. | |
| 2. Add it as a remote: | |
| ```powershell | |
| git remote add hf https://huggingface.co/spaces/Hoctar77/SimpleDocReview | |
| ``` | |
| 3. Authenticate. Use a Hugging Face access token with **write** scope from | |
| https://huggingface.co/settings/tokens as the password when git prompts, or | |
| store it once: | |
| ```powershell | |
| git config credential.helper manager | |
| ``` | |
| 4. Build and push: | |
| ```powershell | |
| .\scripts\deploy.ps1 -AlsoPushOrigin | |
| ``` | |
| A blank Space starts with its own initial commit, so the first push may be | |
| rejected as non-fast-forward. Reconcile once: | |
| ```powershell | |
| git pull --rebase hf main | |
| git push hf main | |
| ``` | |
| ## How the build works | |
| ``` | |
| app/ clean upstream mirror (generated by sync-upstream.ps1) | |
| overlay/ the browser-side API (hand-maintained) | |
| | | |
| v copy + insert two <script> tags into index.html | |
| .build/out/ | |
| | | |
| v copied to the repository root | |
| index.html, static/, sdr-api-core.js, sdr-api-shim.js (committed; HF serves these) | |
| ``` | |
| There is no compiler and no package manager. `build-static.ps1` copies | |
| `app/static/index.html` to the site root, copies the rest of `app/static/` to | |
| `/static/`, copies the two overlay scripts, and inserts their `<script>` tags | |
| immediately before upstream's `app.js` tag. | |
| The repository root is the web root, which is why the site lives there rather | |
| than in a subdirectory β `index.html` asks for `/static/styles.css` by absolute | |
| path. | |
| Everything the build emits is recorded in `.space-build-manifest` and deleted | |
| before the next copy, so nothing stale accumulates. The cleanup refuses to touch | |
| a protected list (`README.md`, `app/`, `scripts/`, β¦) even if the manifest | |
| somehow named one. | |
| ### Why the script tags are inserted rather than committed | |
| The overlay has to load before `app.js`, which fires its first three requests as | |
| soon as it is parsed. The alternative β keeping a modified `index.html` in | |
| `overlay/` β would mean a second copy of an upstream file that silently rots | |
| every time upstream edits its markup. Inserting a tag is a *transform* of | |
| upstream's HTML, so there is nothing to drift. | |
| `build-static.ps1` fails loudly if it cannot find upstream's `app.js` tag, rather | |
| than shipping a page where the overlay never loads. | |
| ## The browser-side API | |
| `overlay/sdr-api-core.js` is a port of `app.py`: every route, the rubric | |
| analyzer, the DOCX text extractor, the status recalculation, the notification | |
| queue, and the record migrations. `overlay/sdr-api-shim.js` supplies what a | |
| browser has and Python does not β IndexedDB, an `XMLHttpRequest` interceptor, and | |
| a click handler for the Download button. | |
| Three things in that split are worth knowing: | |
| - **Requests are intercepted at `XMLHttpRequest`,** not `fetch`, because that is | |
| what upstream's `app.js` uses. The interceptor is a `Proxy` over a real | |
| `XMLHttpRequest`, so anything the shim does not simulate still reaches the | |
| genuine object. `fetch` is covered too, defensively, in case upstream migrates. | |
| - **Download is a click interception, not a request interception.** Upstream | |
| navigates: `window.location.href = "/api/documents/<id>/download"`. A page | |
| cannot cancel that assignment, so the click is caught in the capture phase | |
| before upstream's own handler runs, and the file is handed over as a blob URL. | |
| The shim tracks which document is on screen from the API traffic, because the | |
| click event does not carry the id. | |
| - **DOCX extraction has no dependency.** `app.py` uses `zipfile`; the port reads | |
| the ZIP central directory itself and inflates with `DecompressionStream`, | |
| which every current browser has. ZIP64 is rejected rather than guessed at. | |
| ### Keeping the port honest | |
| A port that silently diverges from the server it mirrors is worse than no port, | |
| so three things guard it. | |
| **A hash guard.** `overlay/ported-from.json` records the SHA-256 of `app.py` and | |
| `static/app.js` as of the last time the port was reviewed. | |
| `build-static.ps1` refuses to build when either changes: | |
| ``` | |
| Upstream changed in files the overlay reimplements: app.py | |
| ``` | |
| Read the upstream diff, carry any behavior change into `sdr-api-core.js`, re-run | |
| the tests, then acknowledge it: | |
| ```powershell | |
| .\scripts\build-static.ps1 -AcceptUpstreamChanges | |
| ``` | |
| This is deliberately annoying. An upstream change to a rubric rule or a status | |
| transition will not reach the Space on its own, and nothing else in a | |
| copy-and-inject build would notice. | |
| **A parity test.** `tests/parity/` replays a 67-request workflow through the real | |
| `app.py` β imported from `app/`, behind a real HTTP server, with its clock and | |
| `uuid4` replaced by counters β and through the JavaScript port in Node, then | |
| diffs every response, the final record store, and the list of stored uploads. | |
| Making the clock sequential means the port has to *call* it the same number of | |
| times in the same order, so a missing timestamp is a failure rather than a value | |
| nobody compares. | |
| One divergence is accepted and printed on every run: `mimetypes.guess_type` | |
| returns `application/octet-stream` for `.docx` on a Windows machine with no | |
| Office mapping, where the port always sends the real OOXML type. | |
| **A browser test.** `tests/browser/` serves the repository root, opens it in | |
| headless Chromium via the DevTools protocol, and runs upload β review β assign β | |
| approve β download β reload through the real UI. That covers what the parity test | |
| cannot: the inserted script tags, the XHR interception, IndexedDB persistence | |
| across a reload, and the download click. | |
| ## What the demo cannot be | |
| Storage is the constraint, and it is not fixable inside a Static Space: | |
| - **Records are per-browser.** IndexedDB is scoped to one origin in one browser | |
| profile. A reviewer opening the same URL sees an empty workspace, so a real | |
| multi-person approval round cannot happen here. | |
| - **Records are disposable.** Clearing site data deletes them, with no export | |
| other than downloading the original files one at a time. | |
| - **Records may not persist at all.** The Space renders in an iframe on | |
| huggingface.co, and a browser that partitions or blocks storage for embedded | |
| third parties leaves the shim with an in-memory store for the length of the | |
| visit. It says so in the notice when that happens. | |
| The Space says all of this in three places: the Space card, a badge next to the | |
| title that does not go away, and a dismissible notice on first load. Keep them | |
| accurate. Someone who mistakes this for a service and runs a real approval round | |
| through it loses the record with no warning, and that is the one failure mode | |
| this deployment can actually cause. | |
| If you need the Space to be the real thing, it needs to run `app.py`, which | |
| means the Docker SDK on a paid plan β and even then, a Space's filesystem is | |
| ephemeral, so `data/reviews.json` and `uploads/` would need a persistent volume | |
| or an external store. The `Dockerfile` here runs upstream unmodified for local | |
| use and as that starting point; the Space does not use it. | |
| ## Troubleshooting | |
| | Symptom | Cause and fix | | |
| | --- | --- | | |
| | Space shows a directory listing or 404 | `app_file: index.html` must be in the front matter and `index.html` must exist at the repo root. Re-run `build-static.ps1`. | | |
| | The page loads but stays empty, console shows 404s for `/api/...` | The overlay did not load. Confirm both `<script>` tags are in the deployed `index.html`, before `app.js`. | | |
| | `sdr-api-core.js did not load` in the console | The tags are in the wrong order. `sdr-api-core.js` must come first. | | |
| | Build fails: "Upstream changed in files the overlay reimplements" | Working as intended. Review the upstream diff, update the overlay, then `-AcceptUpstreamChanges`. | | |
| | Build fails: "Could not find upstream's app.js script tag" | Upstream changed its markup. Update the anchor in `build-static.ps1`. | | |
| | Parity test fails after an upstream change | The port no longer matches `app.py`. The diff output names the exact field. | | |
| | Browser test cannot find a browser | Set `BROWSER` to a `chrome.exe` or `msedge.exe` path. | | |
| | Documents vanish between visits | Expected on a demo. See [What the demo cannot be](#what-the-demo-cannot-be). | | |
| | Uploading a DOCX fails with "appears to be invalid" | The port rejects ZIP64 archives and unsupported compression methods. Re-save the file from Word. | | |
| | Push rejected, non-fast-forward | The Space has commits this repo lacks: `git pull --rebase hf main`. | | |
| | `robocopy` exit code 8 or higher | A real copy failure β a file was locked. Close anything watching `app/` or `.build/` and re-run. | | |