Spaces:
Running
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
Deploying to Hugging Face
This repo deploys 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 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.pyowns 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.
The everyday workflow
After you change something in C:\dev\SimpleDocReview and commit it there:
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:
.\scripts\deploy.ps1 -DryRun
Individual steps, when you want them:
.\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
Create the Space at https://huggingface.co/new-space β choose the Static SDK and the Blank template. Name it
SimpleDocReview.Add it as a remote:
git remote add hf https://huggingface.co/spaces/Hoctar77/SimpleDocReviewAuthenticate. 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:
git config credential.helper managerBuild and push:
.\scripts\deploy.ps1 -AlsoPushOriginA blank Space starts with its own initial commit, so the first push may be rejected as non-fast-forward. Reconcile once:
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, notfetch, because that is what upstream'sapp.jsuses. The interceptor is aProxyover a realXMLHttpRequest, so anything the shim does not simulate still reaches the genuine object.fetchis 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.pyuseszipfile; the port reads the ZIP central directory itself and inflates withDecompressionStream, 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:
.\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. |
| 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. |