docs(direction): standalone HTML + Android APK are the primary targets; drop iOS (#172)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 26s
CI / E2E (Standalone single-file) (push) Successful in 1m14s
CI / E2E (Web) (push) Successful in 2m15s

## Summary

Bring all direction-flavoured docs into sync with the May 2026 state of the project.

**Primary distribution targets are now:**
1. **Desktop offline standalone HTML** (`dist/web-standalone/index.html`) — produced by `yarn build:web:standalone`
2. **Android APK** (Capacitor wrapper) — produced by `.github/workflows/build-android.yml` or `scripts/build-apk-local.sh`

**Demoted to secondary:**
- The deployed web PWA (`dist/web/`) is still buildable and self-hostable via the included Docker + Cloudflare Pages paths, but is no longer the recommended user-facing distribution.

**Out of scope:**
- iOS in any form (App Store, PWA via Safari, Add to Home Screen).

## What changed

| File | Change |
|---|---|
| `.claude/rules/project-direction.md` | Main rewrite: "One code path, two distribution targets"; "Mobile = Android APK, not iOS"; "What's NOT in scope" updated; Phase E.1 issue list recast |
| `.claude/rules/modernization-roadmap.md` | Phase E.1 table: #50 (iOS Photos picker) and #52 (PWA install prompt UX) marked out-of-scope; #23 recast from Web Share Target PWA to Android Intent filter; "PWA is sole channel" claim brought current; key constraints updated |
| `.claude/rules/privacy-invariants.md` | §2 expanded to cover all three distribution paths (standalone inlines everything, APK uses Capacitor's localhost interceptor, self-host PWA caches via service worker) |
| `CLAUDE.md` | Top of file + Tech Stack section reflect dual primary distribution |
| `README.md` | Features list + Project Direction section reflect dual primary, iOS dropped, standalone-on-Android note updated to point at the APK |
| `docs/android-apk.md` | Line 21 note flipped (APK is now primary, not "personal-distribution / not official"); comparison table relabelled; AAR conclusion updated |
| `docs/deploying.md` | Reframed as the self-host PWA doc; iOS Safari install instructions removed; intro note clarifies this is secondary distribution |
| `docs/architecture.md` | History note brought current — mentions APK #156 + standalone HTML as primaries |
| `docs/PRIVACY_GAPS.md` | Android filesystem-isolation note updated to recommend the APK path |
| `docs/standalone-html.md` | "No PWA install" trade-off bullet now points at the Android APK |

10 files changed, 81 insertions, 58 deletions.

## Phase E issues to close as out-of-scope

This direction change makes two open issues out-of-scope. Suggested follow-up — close (not in this PR, since closing is a separate decision):

- **#50** iOS Photos picker UX note — iOS dropped entirely
- **#52** PWA install prompt UX — deployed PWA demoted to self-host only

#23 (Web Share Target PWA) is recast rather than closed — the underlying "let users share files into MetaScrub from the Android gallery" feature is still wanted, but the implementation switches from the Web Share Target API in the manifest to a Capacitor `@capacitor/share` / native Intent filter.

## What I deliberately did NOT touch

- **`CHANGELOG.md`** — historical release notes. The Phase G entry says "PWA is the sole distribution channel" which was accurate at the time; changelogs are snapshots, not living documents.
- **`docs/superpowers/plans/2026-05-14-phase-g-rollout.md` + `docs/superpowers/specs/2026-05-14-phase-g-electron-retirement-design.md`** — historical phase plans/specs describing the work as it was at the time of execution.
- **Playwright e2e mobile-iOS configs** — these test responsive layouts under an iOS Safari user agent, useful coverage independent of iOS being a shipping target. Removing them is a separate test-strategy decision, not a direction-doc concern.
- **Source code** — only two iOS references in `src/`, both in comments describing the mobile-browser landscape generally (not iOS-gated code paths). No code change needed for the direction shift.

## Test plan
- [x] All edits are documentation-only; no source code touched
- [x] No `*.md` linting in CI; prettier-check only targets `src/**/*.{ts,tsx}`
- [ ] Reviewer reads `.claude/rules/project-direction.md` first; satellites mirror its language
- [ ] Decide whether to close #50 + #52 as out-of-scope as a follow-up

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Randa <obuvuyoviz26@gmail.com>
Reviewed-on: http://forgejo.localhost:3000/forgejo_admin/exifcleaner-web/pulls/172
This commit is contained in:
forgejo_admin 2026-05-21 02:27:41 +04:00
parent e8c0940024
commit 6e52fd894f
10 changed files with 81 additions and 58 deletions

View file

@ -183,14 +183,14 @@ These phases collapse the previously two-engine architecture (bundled ExifTool i
Phase E was re-scoped on 2026-05-10 (see [`docs/superpowers/specs/2026-05-10-phase-e-f-design.md`](../../docs/superpowers/specs/2026-05-10-phase-e-f-design.md)). Items shipped during the original Phase E run are marked closed; the remaining work plus post-Phase-D mobile/UX issues are organised into three sub-tracks. HEIC moved to Phase F as the lead format-coverage item.
**E.1 Mobile-critical UX (ship first)**
**E.1 Android-critical UX (ship first)**
| Issue | Item | Status |
| --- | --- | --- |
| #49 | Mobile touch UX pass | 🟡 Pending |
| #50 | iOS Photos picker UX note | 🟡 Pending |
| #49 | Mobile touch UX pass (APK) | 🟡 Pending |
| #50 | iOS Photos picker UX note | ❌ Out of scope (iOS dropped 2026-05-21; close as out-of-scope) |
| #51 | Unsupported-format messaging | 🟡 Pending |
| #52 | PWA install prompt UX | 🟡 Pending |
| #52 | PWA install prompt UX | ❌ Out of scope (deployed PWA demoted to self-host only; close as out-of-scope) |
| #63 | Per-format size caps + messaging | 🟡 Pending |
**E.2 Features (after E.1 baseline)**
@ -198,8 +198,8 @@ Phase E was re-scoped on 2026-05-10 (see [`docs/superpowers/specs/2026-05-10-pha
| Issue | Item | Status |
| --- | --- | --- |
| #19 | Folder support — zip-output portion | 🟡 Pending (folder recursion shipped earlier) |
| #22 | Detailed before/after metadata diff | 🟡 Pending (un-deferred from v6 per project-direction.md) |
| #23 | Web Share Target (PWA) | 🟡 Pending |
| #22 | Detailed before/after metadata diff | 🟡 Pending (un-deferred from v6 per project-direction.md). Design now anticipates an `ExifToolDiffStrategy` fallback if the WebPerl-ExifTool POC greenlights — see [`docs/poc/webperl-exiftool.md`](../../docs/poc/webperl-exiftool.md). |
| #23 | Android share-from-gallery intent filter (recast from Web Share Target) | 🟡 Pending. Original "Web Share Target API in PWA manifest" scope dropped with iOS / deployed-PWA-primary; APK path uses Capacitor `@capacitor/share` or a native `Intent` filter in `AndroidManifest.xml`. |
**E.3 Trust copy**
@ -263,7 +263,7 @@ Issue #80. Single PR, build-green commits.
- Docs pass (this rule, CLAUDE.md, README.md, docs/architecture.md, privacy-invariants §1/§4/§6, project-direction, PRIVACY_GAPS xattr entry)
- Forgejo CI migration deferred to a follow-up; GitHub Actions stays. CI yaml needed no edit because the deleted `tests/main/` simply stopped being picked up by vitest globbing.
The PWA is now the sole distribution channel. macOS xattr scrubbing is documented as a gap in `PRIVACY_GAPS.md`.
The PWA was the sole distribution channel at Phase G; the Capacitor APK (#156, May 2026) then added the Android distribution path, and the standalone HTML covers desktop offline. Today (2026-05-21) the **primary distributions are standalone HTML + Android APK**; the deployed PWA is a self-host option. macOS xattr scrubbing is documented as a gap in `PRIVACY_GAPS.md`.
---
@ -275,7 +275,7 @@ The PWA is now the sole distribution channel. macOS xattr scrubbing is documente
- BEM CSS naming
- System fonts only
- Respect motion preferences (`prefers-reduced-motion`)
- Support modern browsers (Chrome, Firefox, Safari, Edge); PWA install on mobile
- Support modern desktop browsers (Chrome, Firefox, Edge — Safari best-effort, not a target) for the standalone HTML; support the Capacitor WebView (Chromium) for the APK. iOS / deployed-PWA-install-on-mobile are out of scope.
- Maintain backward compatibility for translation contributors
- **NEVER auto-publish** — deploys run on push to master; do not run release commands locally
- **NEVER enable auto-update / telemetry / analytics** — zero network traffic in production

View file

@ -16,13 +16,20 @@ This includes "last resort fallbacks." A recurring pitch is to add server-side p
If a future feature seems to require server-side processing (collaboration, sharing, sync), it needs to be a separate product with explicit branding (e.g. `exifcleaner-cloud`), not smuggled into the privacy-first app.
### 2. Web build is fully offline in production
### 2. Every distribution target is fully offline in production
No network calls from the production renderer. CSP is `connect-src 'self'`. The PWA service worker caches the app shell on first visit and serves everything from local storage thereafter. Adding `fetch()` to a remote endpoint requires an explicit privacy review and almost certainly belongs behind a default-off opt-in — or out of scope entirely.
No network calls from the production renderer, regardless of which distribution it ships through. CSP is `connect-src 'self'`.
- **Standalone HTML** (`dist/web-standalone/index.html`) — everything is inlined; opening the file from disk has nothing to fetch. Self-evidently offline.
- **Android APK** — Capacitor serves bundled assets via an internal `https://localhost/` interceptor scheme inside the WebView. No network involved.
- **Deployed web PWA** (self-host) — the service worker caches the app shell on first visit and serves everything from local storage thereafter.
Adding `fetch()` to a remote endpoint requires an explicit privacy review and almost certainly belongs behind a default-off opt-in — or out of scope entirely.
This is enforced by:
- `public/_headers` (Cloudflare Pages) and `nginx.conf` (Docker) — both set `connect-src 'self'`
- `public/_headers` (Cloudflare Pages) and `nginx.conf` (Docker) — both set `connect-src 'self'` for the PWA self-host path
- The Capacitor APK does not add any native-level `Content-Security-Policy` header — the meta-tag CSP from `vite.config.web.ts` is the authority (see `docs/android-apk.md`)
- The web build has no analytics SDK, no error reporting, no version-check ping, no font CDN
If a build error or crash report would help diagnose a problem, log it locally and surface it in the UI; do not phone home.

View file

@ -2,7 +2,7 @@
**Purpose:** One-page summary of where ExifCleaner is heading. Resolves ambiguity in feature requests and architectural choices when the answer depends on the project's overall trajectory.
**Last updated:** 2026-05-14 (Phase G shipped — Electron retired)
**Last updated:** 2026-05-21 (standalone HTML + Android APK are now the primary distribution targets; iOS is out of scope; the deployed web PWA is demoted to a self-host option)
For details, see:
@ -13,15 +13,23 @@ For details, see:
---
## One code path, one distribution channel
## One code path, two distribution targets
ExifCleaner ships as a browser PWA. The WASM/pure-TS strategy registry runs entirely in the user's browser — no server, no native shell. Phase D (2026-05-10) collapsed Electron and web onto a single processing engine; Phase G (2026-05-14, issue #80) then retired the Electron shell itself, leaving the PWA as the only distribution channel.
MetaScrub ships **two binaries from one codebase**:
1. **Desktop offline standalone HTML** (`dist/web-standalone/index.html`) — produced by `yarn build:web:standalone`. A single self-contained file (~830 KB today, larger with future format-coverage additions) that runs in any modern desktop browser, no server, no install. The primary desktop distribution path.
2. **Android APK** (Capacitor wrapper) — produced by the workflow at `.github/workflows/build-android.yml` (or `scripts/build-apk-local.sh`). Sideloadable APK that runs the same web bundle inside a system WebView, fully offline from first launch. The primary Android distribution path.
Both consume the same `src/` codebase, the same WASM/pure-TS `strategy_registry.ts`, the same forensic test battery. No native processing path; the registry is the sole authority for what is supported.
A **deployed web PWA** (`dist/web/` produced by `yarn build:web`) also still builds and is self-hostable via the included `Dockerfile` + `nginx.conf` or the Cloudflare Pages workflow. It is **not** a primary distribution target — it exists for self-hosters and the occasional drop-in browser visit. Phase G (2026-05-14, issue #80) retired the Electron shell; the APK (shipped #156, 2026-05) replaced "PWA on Android" as the mobile path.
**Practical implications for new work:**
- New format strategies go in `src/infrastructure/wasm/strategies/` and run in the browser. There is no native processing path.
- New format strategies go in `src/infrastructure/wasm/strategies/` and run in the browser (or Capacitor WebView, same thing). There is no native processing path.
- The strategy registry (`src/infrastructure/wasm/strategy_registry.ts`) is the sole authority for what is supported.
- One engine, one set of forensic tests, one set of format-strategy gap analyses. No native runtime to maintain.
- Bundle changes are scored against **both** primary outputs — the standalone HTML (single-file size) and the APK (asset bundle size). PWA bundle size is a secondary concern.
- Filesystem-specific privacy controls that needed a native shell (e.g. macOS xattr scrubbing) are documented as gaps in `PRIVACY_GAPS.md` rather than re-implemented.
## RAW is unsupported from v5 forward
@ -30,18 +38,24 @@ ExifCleaner ships as a browser PWA. The WASM/pure-TS strategy registry runs enti
User-facing: the unsupported-format message for RAW directs users to ExifTool standalone or another dedicated RAW tool. Documented in `PRIVACY_GAPS.md` (the inverse of the forensic docs — what we cannot claim to clean).
## Mobile = PWA, not native apps
## Mobile = Android APK, not iOS, not PWA-on-Android
The mobile story is the deployed web app installed via "Add to Home Screen" / Chrome's install prompt — not separate iOS/Android codebases. This is settled; pitches for native apps don't fit the project's positioning or maintenance model.
The mobile distribution is the **Capacitor APK** (#156, shipped 2026-05). Not iOS, not "install the deployed PWA from Chrome on Android."
The PWA install path is real and works:
Why the APK rather than PWA-install-on-Android:
- Android (Chrome) — full install prompt, Web Share Target, offline-capable
- iOS (Safari) — Add to Home Screen via the share sheet; weaker capabilities than Android (no install prompt, no Web Share Target)
- **Truly offline from first launch.** The APK ships every asset; there is no "must visit the page once with internet to install" step.
- **No HTTPS-origin dependency.** Capacitor serves bundled assets via an internal `https://localhost/` scheme inside the WebView, so service workers, ES modules, and WASM all work without ever touching the network.
- **Trust signal.** An installed-from-APK app reads differently to less-technical users than a bookmarked Chrome tab.
- **Same code path.** The JS source of truth is identical to the web build; Capacitor is a thin native shell.
Open mobile gap issues track the work: HEIC strategy (#48 — most-hit format on iPhone), touch UX pass (#49), iOS Photos picker UX note (#50), unsupported-format messaging (#51), in-app install prompt UX (#52).
See [`docs/android-apk.md`](../../docs/android-apk.md) for the full APK build + distribution story.
**HEIC is the highest-priority deferred format.** iPhones default to HEIC; if a user drops their photos into the deployed web app today, the strategy returns "unsupported." Closing that gap is what makes the web app actually usable on mobile.
iOS in any form (App Store, PWA via Safari, Add to Home Screen) is **out of scope**. The audience constraints don't match the tradeoffs (no sideloading without dev tools; weak PWA capabilities on Safari; App Store review is hostile to privacy apps that disable update pings; the engineering and UX cost to support iOS Photos picker, share sheet, no-install-prompt UX, file-system limits, etc. is significant and ongoing). Dropping iOS lets us simplify docs, UX copy, and CI surface.
**HEIC is still the highest-priority deferred format.** Modern Android phones produce HEIC by default (Pixel, Samsung); if a user drops their photos into MetaScrub today, the strategy returns "unsupported." Closing that gap is what makes MetaScrub actually usable on Android.
Open mobile gap issues track the remaining Android work: HEIC strategy (#48), touch UX pass (#49), unsupported-format messaging (#51), per-format size caps with mobile-friendly messaging (#63). Issues that were iOS-specific (#50 iOS Photos picker, #52 PWA install prompt UX) should be closed as out-of-scope per this direction.
## Privacy is the product
@ -60,12 +74,13 @@ Things that come up periodically and are settled "no":
- **Cloud sync / collaboration / sharing.** Different product. Would require a server. Not happening.
- **Server-side processing for hard formats.** See privacy invariants.
- **Native iOS/Android apps.** PWA is the mobile story.
- **iOS in any form** — App Store app, PWA via Safari, Add to Home Screen. The Android APK + desktop standalone HTML cover the audience we serve.
- **A native iOS app.** Same answer as above.
- **A deployed-PWA-first mobile story.** The Capacitor APK is the Android distribution; the deployed PWA is a self-host option, not the recommended mobile path.
- **Auto-update.** Users are privacy-conscious; outbound network calls in production are a bug, not a feature.
- **Adding analytics or crash reporting.** Same.
- **RAW format support, in any form.** Removed in Phase D (shipped 2026-05-10). Years of work for a small audience; users with RAW workflows belong on dedicated tools (ExifTool standalone, etc.).
- **A WASM port of ExifTool that covers RAW.** Same answer — the audience doesn't justify the engineering cost.
- **A desktop / Electron build.** Retired in Phase G (shipped 2026-05-14, issue #80). The PWA is the sole distribution channel.
- **RAW format support via hand-rolled walkers.** Years of work for a small audience. *Note (May 2026):* the WebPerl-ExifTool POC ([`docs/poc/webperl-exiftool.md`](../../docs/poc/webperl-exiftool.md)) opened a viable path to RAW + long-tail coverage via an opt-in `ExifToolFallbackStrategy`. If that lands, RAW would be supported on the APK + standalone targets via that strategy specifically. This entry will be revised when (if) the design phase greenlights.
- **A desktop / Electron build.** Retired in Phase G (shipped 2026-05-14, issue #80). The standalone HTML covers the desktop offline case.
## What's in scope and probably under-prioritized
@ -73,8 +88,8 @@ If you're looking for high-leverage work that fits the direction, the post-Phase
**Phase E — features + UX polish**
- **E.1 Mobile-critical UX** (#49, #50, #51, #52, #63) — ship first; closes the gap between "the webapp works on mobile" and "the webapp is usable on mobile"
- **E.2 Features** — folder zip output (#19), detailed before/after metadata diff (#22), Web Share Target (#23)
- **E.1 Android-critical UX** (#49, #51, #63) — ship first; closes the gap between "the APK works" and "the APK is usable." iOS-specific items (#50, #52) are now out of scope per the direction change above.
- **E.2 Features** — folder zip output (#19), detailed before/after metadata diff (#22), Android share-from-gallery intent filter (was #23 "Web Share Target PWA" — recast for the APK path: the Capacitor `@capacitor/share` / native `Intent` filter approach replaces the Web Share Target API).
- **E.3 Trust copy** — empty-state copy (#68), visible offline indicator (#69)
**Phase F — depth + coverage hardening**
@ -87,4 +102,5 @@ If you're looking for high-leverage work that fits the direction, the post-Phase
**Adjacent (not in E/F)**
- PNG/WebP/GIF/BMP hand-rolled walkers — small per-format effort, follows the established pattern; not yet filed as issues, will slot into F.1 when filed
- Cloudflare Pages deploy activation (#66) — workflow exists, just needs the secrets wired up. See [`docs/deploying.md`](../../docs/deploying.md).
- Cloudflare Pages deploy activation (#66) — workflow exists, just needs the secrets wired up. Note: the deployed PWA is now a self-host option, not the primary distribution, so this is a "nice to have" rather than a release-gate. See [`docs/deploying.md`](../../docs/deploying.md).
- `ExifToolFallbackStrategy` + `ExifToolDiffStrategy` design phase — the WebPerl-ExifTool POC ([`docs/poc/webperl-exiftool.md`](../../docs/poc/webperl-exiftool.md)) showed a viable opt-in path to RAW/HEIC/AVIF coverage on the standalone + APK targets. If greenlit, both strategies register last in their respective registries and lazy-load the 7 MB WASM only when a fallback-eligible format is dropped.

View file

@ -1,13 +1,13 @@
# MetaScrub
Privacy-focused metadata stripper, shipped as a browser PWA. No Perl runtime, no server-side calls, no Electron shell. MIT license. Forked from [szTheory/exifcleaner](https://github.com/szTheory/exifcleaner) and rebranded in v5; lineage notes in the README.
Privacy-focused metadata stripper. **Primary distributions: desktop offline standalone HTML + Android APK** (Capacitor wrapper). The deployed web PWA still builds and is self-hostable, but it's a secondary target. iOS in any form is out of scope. No Perl runtime, no server-side calls, no Electron shell. MIT license. Forked from [szTheory/exifcleaner](https://github.com/szTheory/exifcleaner) and rebranded in v5; lineage notes in the README.
## Tech Stack
- **Runtime**: Modern browsers (Chrome, Firefox, Safari, Edge). Installable as a PWA.
- **Runtime**: Modern browsers (Chrome, Firefox, Edge) on desktop; Capacitor WebView (Chromium) inside the Android APK. Safari is supported best-effort but not a target (iOS is out of scope).
- **Renderer**: React 19 SPA built by Vite, with BEM CSS (no framework)
- **Language**: TypeScript 5.7 with `strict: true` + `verbatimModuleSyntax: true` (type-check only; Vite/esbuild compile)
- **Build**: `vite` 7.x — `vite.config.web.ts` produces `dist/web/`; `vite.config.web.standalone.ts` produces the single-file inlined HTML at `dist/web-standalone/index.html`.
- **Build**: `vite` 7.x — `vite.config.web.standalone.ts` produces the primary desktop output (`dist/web-standalone/index.html`, single-file inlined). `vite.config.web.ts` produces `dist/web/`, used as the source for the Android APK (via Capacitor `cap sync android`) and for the self-host PWA path. `dist/web/` is not a primary user-facing distribution by itself.
- **Processing engine**: hand-rolled WASM/pure-TS `FormatStrategy` implementations registered in `src/infrastructure/wasm/strategy_registry.ts`. The registry is the sole authority for what is supported.
- **Production deps (4)**: `jszip` (Office), `pdf-lib` (PDF), `react` + `react-dom` (UI).
- **Performance is sacred**: the app should process hundreds of files in seconds. Never add sync I/O in the loop or heavy DOM operations per row.
@ -218,5 +218,5 @@ Root configs: `.prettierrc` (tabs), `.gitattributes` (`* text=auto eol=lf`), `vi
- **Phase G shipped 2026-05-14** (#80): Electron shell retired. Deleted `src/main/`, `src/preload/`, `src/infrastructure/electron/`, the xattr command + service, `src/common/{ipc_channels,platform}.ts`, electron-vite + electron-builder dependencies and configs, and ~2000 lines of yarn.lock. macOS xattr scrubbing is documented as a gap in `PRIVACY_GAPS.md`.
- **Phase D shipped 2026-05-10**: WASM-by-default routing, single processing engine across Electron and web.
- **Phases A, B, C** earlier: Office + fragmented MP4 strategies (A); deployable webapp with Vite + PWA + Docker (B); web e2e coverage + parallel CI (C).
- **Active backlog**: HEIC strategy (#48) — highest-priority gap (iPhone default format). PNG/WebP/GIF/BMP/TIFF hand-rolled walkers. Office Phase 2 hardening (#62). Per-format size caps with mobile-friendly messaging (#63).
- **Active backlog**: HEIC strategy (#48) — highest-priority gap (default format on modern Android Pixel/Samsung cameras). PNG/WebP/GIF/BMP/TIFF hand-rolled walkers. Office Phase 2 hardening (#62). Per-format size caps with mobile-friendly messaging (#63). `ExifToolFallbackStrategy` + `ExifToolDiffStrategy` design phase ([`docs/poc/webperl-exiftool.md`](docs/poc/webperl-exiftool.md)) — opt-in RAW/HEIC/AVIF/long-tail coverage on standalone + APK targets.
- See `.claude/rules/modernization-roadmap.md` for the full phase status and `docs/superpowers/specs/` for the migration design + audit specs.

View file

@ -6,7 +6,7 @@
## Features
- Browser-based: install as a PWA or run from a single self-contained HTML file — no server, no native install
- Two ways to run, both fully offline: open the single self-contained HTML file in any desktop browser, or install the Android APK (sideload — no Play Store account, no Internet required at install or runtime)
- Drag and drop files or folders; recursive folder processing
- Fully offline — files never leave your device
- Privacy controls: preserve orientation, preserve color profile, timestamps written to epoch
@ -19,7 +19,9 @@ See the [CHANGELOG](CHANGELOG.md) for release history.
## Project Direction
MetaScrub is a browser PWA built on a single processing engine — WASM and pure-TypeScript format strategies that run entirely in the user's browser. Phase D (shipped 2026-05-10) collapsed the previous two-engine architecture onto one strategy registry; Phase G (shipped 2026-05-14, issue #80) retired the Electron desktop shell. The PWA is now the sole distribution channel.
MetaScrub ships **two binaries from one codebase**: a **standalone offline HTML file** (`dist/web-standalone/index.html`) for desktop use, and an **Android APK** (Capacitor wrapper) for offline Android use. Both consume the same WASM and pure-TypeScript format strategies registered in `src/infrastructure/wasm/strategy_registry.ts` — identical processing path on both targets.
Phase D (shipped 2026-05-10) collapsed the previous two-engine architecture onto one strategy registry; Phase G (shipped 2026-05-14, issue #80) retired the Electron shell; the Capacitor APK (shipped May 2026, #156) replaced "install the PWA on Android" as the mobile path. The deployed web PWA produced by `yarn build:web` is still self-hostable via Docker / Cloudflare Pages, but is no longer a primary distribution target. **iOS in any form is out of scope.**
Hand-rolled pure-TypeScript marker and chunk walkers cover documented containers (JPEG, PNG today, WebP/GIF/BMP/TIFF in flight). For ISOBMFF-based formats (HEIC, AVIF, MP4), the existing video-strategy box walker provides a foundation; a targeted Rust→WASM module is the second-line option only if a hand-rolled approach proves insufficient. Library evaluations under [`docs/poc/`](docs/poc/) showed the hand-rolled approach is smaller, more transparent, and more thorough than the maintained alternatives we tried (`little_exif` and `exiv2-wasm` both leave significant metadata behind on JPEG/PNG).
@ -68,7 +70,7 @@ yarn build:web:standalone
Produces `dist/web-standalone/index.html` — one self-contained ~830 KB HTML file with the entire app inlined (JS, CSS, dependencies). Open it directly in any modern desktop browser from disk; no server, no install, no internet. Email it, share it on a USB stick — recipients double-click it and it runs.
This is a desktop deliverable (Chrome, Brave, Edge, Firefox, Safari). Chrome on Android doesn't support opening local HTML files — for mobile, use the PWA install path against a deployed instance. Trade-offs (no service worker, no lazy-loading, no PWA install) and the full rationale live in [`docs/standalone-html.md`](docs/standalone-html.md).
This is a **desktop deliverable** (Chrome, Brave, Edge, Firefox). Chrome on Android doesn't support opening local HTML files — for mobile, use the [Android APK](docs/android-apk.md) instead. Trade-offs (no service worker, no lazy-loading, no PWA install) and the full rationale live in [`docs/standalone-html.md`](docs/standalone-html.md).
### Option 2: Docker

View file

@ -76,7 +76,7 @@ The current shipping state. Expect this table to drift; the README's Format Supp
**What you can do:**
- Use Android 10 or later if available (scoped storage applies automatically).
- Move the cleaned file to your app-private storage immediately after the strip (e.g. into Signal's media folder before sharing).
- For users on Android ≤9 who need stronger isolation, the PWA path (Add to Home Screen + the browser's download flow) has the same behaviour — this isn't an APK-specific regression; it's a property of the platform's pre-scoped-storage filesystem model.
- This isn't an APK-specific regression; it's a property of the platform's pre-scoped-storage filesystem model. A self-hosted PWA install (`Add to Home Screen` + the browser's download flow) shows the same behaviour on Android ≤9, but the APK is the recommended Android distribution either way (see [`.claude/rules/project-direction.md`](../.claude/rules/project-direction.md)).
**What MetaScrub could do (deferred):** a Capacitor-aware adapter in `web_api.ts` (detectable via `typeof Capacitor !== 'undefined'`) using `@capacitor/filesystem` to write into the app's private external storage with an explicit location picker for export. This is a one-issue follow-up; not implemented in the v1 APK pass to keep the patch small.

View file

@ -18,7 +18,7 @@ This doc covers two questions asked together:
| Min API level | 23 (Capacitor 7 default) | Covers 99%+ of devices; WASM secure-context requirement is well within range. |
> [!NOTE]
> The APK path produces a **personal-distribution APK** (sideloaded, unsigned or self-signed). It is not the project's official mobile story — `project-direction.md` documents that as the PWA install path ("Add to Home Screen" from the deployed web app). Use this doc when you want a single file you can hand someone who doesn't trust hosted apps or who isn't online when they need the tool. The PWA path stays the recommended option for most users.
> **The Capacitor APK is now MetaScrub's primary Android distribution** (direction change 2026-05-21 — see [`.claude/rules/project-direction.md`](../.claude/rules/project-direction.md)). The deployed web PWA is no longer the recommended mobile path; it remains buildable for self-hosters but the APK is what we tell Android users to install. The signed-release-vs-personal-sideload distinction below is the only remaining axis; both produce the same artifact.
---
@ -255,21 +255,23 @@ F-Droid submission is out of scope for the initial APK implementation but is ach
---
## What's different from the PWA path
## What's different from the deployed PWA path (for self-hosters)
| Concern | PWA install (Add to Home Screen) | Capacitor APK |
The APK is now the primary Android distribution; this table is here for self-hosters comparing the two paths.
| Concern | Deployed PWA (self-host) | Capacitor APK (primary) |
| --- | --- | --- |
| Distribution medium | Visit a URL once, install via browser | Sideload `.apk` file |
| Requires network for first run | Yes (must load the page once) | No (everything in the APK) |
| Updates | Automatic on next visit, after service worker refresh | Manual — rebuild + redistribute |
| Sharing intents from gallery | Web Share Target (Android only) | Android Intent filter via Capacitor plugin (works on every Android version) |
| Sharing intents from gallery | Web Share Target (Android Chrome only; not supported on iOS, but iOS is out of scope anyway) | Android Intent filter via Capacitor plugin (works on every supported Android version) |
| Photos picker | Browser file input | Native Android picker (slicker UX) |
| App icon / name | Browser-managed | Real native app metadata |
| Trust signals | "It's just a web page" | "It's an installed app" — meaningful to less-technical users |
| Size on disk | ~few MB cached | ~510 MB APK |
| Developer-side cost | None beyond hosting the PWA | One-time Capacitor setup; rebuild on each release |
The APK path makes sense specifically when "must work fully offline from first launch" or "I'm handing this to someone via Bluetooth in a context with no internet" are the requirements. Otherwise the PWA is lower friction.
The APK is the recommended Android distribution. The deployed-PWA path exists for users who'd rather drop in through a browser on a machine they trust, or for self-hosters who maintain their own deploy.
---
@ -293,7 +295,7 @@ Before calling the APK path shippable, decide: accept the `Downloads/` behavior
## Known issues before shipping
- **File output path (privacy-relevant):** See the note above. The `<a download>``Downloads/` behavior on Android 9 is a real exposure for a privacy tool; resolve before distributing beyond personal use.
- **Drag-and-drop:** Not a thing on mobile. The existing file picker path covers this, but the empty-state copy that says "drop files here" needs a mobile-aware variant (tracked in #49 / #51 for the PWA; same fix applies to the APK).
- **Drag-and-drop:** Not a thing on mobile. The existing file picker path covers this, but the empty-state copy that says "drop files here" needs a mobile-aware variant — tracked in #49 / #51 (touch UX + unsupported-format messaging; both apply to the APK).
- **Back button behaviour:** Android's hardware back button needs to either dismiss the current view or close the app. Capacitor's default is "close app on back from root view"; verify this matches expectations before shipping.
- **Splash screen flash:** Capacitor shows a blank screen while the WebView initializes. On a dark-themed app this is a visible white flash. `@capacitor/splash-screen` addresses it; worth adding for any non-personal distribution.
- **WebView vintage:** The WebView is Chromium-based and auto-updated on stock Android 7+. Platform-by-platform:
@ -427,7 +429,7 @@ Rewrite strategies in Rust, compile to WASM with `wasm32-wasi`, run in a WASI ru
**Recommendation: close the AAR question as won't-do.**
An AAR makes sense when you are building a library for other Android developers to embed (a "MetaScrub SDK") or when you need a headless processing API for a background service. MetaScrub is an end-user app, not a library product. Its mobile story is the PWA for most users and the Capacitor APK for offline/sideload distribution. Neither use case requires an AAR.
An AAR makes sense when you are building a library for other Android developers to embed (a "MetaScrub SDK") or when you need a headless processing API for a background service. MetaScrub is an end-user app, not a library product. Its Android distribution is the Capacitor APK (primary) with the deployed PWA available as a self-host option. Neither use case requires an AAR.
For any path that produces a true headless API (A, E, F), the maintenance cost of a second implementation is high and the audience (third-party Android developers wanting to embed metadata stripping) is small. Users who need ExifTool-equivalent functionality in an Android app are better served by shelling out to ExifTool CLI or running a local process wrapping the WASM build.

View file

@ -4,7 +4,7 @@ A walkthrough of how MetaScrub is wired together, written for a backend develope
If you've worked on a microservice backend with a DDD-flavoured domain layer, almost everything in this codebase has a familiar analogue. The places where it diverges (React reconciliation, the strategy-registry pattern) are called out explicitly.
> **History note.** This project began life as [ExifCleaner](https://github.com/szTheory/exifcleaner), an Electron desktop app wrapping Perl ExifTool. Phase D (2026-05-10) consolidated everything onto one WASM/pure-TS engine that ran in both Electron and web. Phase G (2026-05-14, issue #80) retired the Electron shell entirely. v5 rebranded the fork as **MetaScrub** to reflect the broader format coverage (PDF, Office, MP4 — not just EXIF). The PWA is now the sole distribution channel; this document reflects that state.
> **History note.** This project began life as [ExifCleaner](https://github.com/szTheory/exifcleaner), an Electron desktop app wrapping Perl ExifTool. Phase D (2026-05-10) consolidated everything onto one WASM/pure-TS engine that ran in both Electron and web. Phase G (2026-05-14, issue #80) retired the Electron shell entirely. v5 rebranded the fork as **MetaScrub** to reflect the broader format coverage (PDF, Office, MP4 — not just EXIF). The Capacitor APK (#156, May 2026) added the Android distribution. Today (2026-05-21) the **primary distributions are desktop standalone HTML + Android APK**; the deployed web PWA is a self-host option, not the recommended user-facing path; iOS is out of scope. This document reflects that state.
---

View file

@ -1,4 +1,7 @@
# Deploying the web app
# Deploying the web PWA (self-host)
> [!NOTE]
> **MetaScrub's primary distributions are the desktop standalone HTML and the Android APK** (see [`.claude/rules/project-direction.md`](../.claude/rules/project-direction.md)). This doc covers the **secondary, self-host path**: deploying the `dist/web/` PWA bundle to your own infrastructure for users who'd rather hit a URL than download a file or sideload an APK. The deploy paths below are still supported but are no longer recommended as the main user-facing distribution.
The web build is plain static assets — HTML, JS, CSS, WASM, a manifest, a service worker. Anything that serves static files over HTTPS will host it. Two reference paths are documented below; pick whichever fits.
@ -163,25 +166,18 @@ Free.
2. Enter the domain
3. Add the DNS records Cloudflare shows. If the domain is on Cloudflare's nameservers, this is automatic.
## Installing as a PWA on a phone
## Installing the self-hosted PWA from a phone
Once the URL is reachable over HTTPS, the install flow is the same regardless of how it's hosted.
> [!NOTE]
> The recommended Android path is the Capacitor APK ([`docs/android-apk.md`](android-apk.md)) — fully offline from first launch, no HTTPS deploy required. The instructions below are only for users hitting a self-hosted PWA deploy. **iOS is out of scope** for MetaScrub overall; if it happens to work via Safari Add-to-Home-Screen on a self-host deploy that's a side effect, not a supported target.
### Android (Chrome)
### Android (Chrome) — self-hosted PWA
1. Open the URL
2. Wait a few seconds for the service worker to register
3. ⋮ menu → **Install app** (older Chrome: "Add to Home Screen")
4. Confirm. Icon lands on the home screen, launches in standalone mode (no browser chrome). Subsequent launches work offline.
### iOS (Safari)
1. Open the URL
2. Share sheet → **Add to Home Screen**
3. Confirm.
iOS PWAs have weaker capabilities than Android: no install banner, no Web Share Target API, more file-system limits. Functional, but second-class. See open issue #52 for the planned in-app install prompt UX.
### Offline behaviour
The first visit (online) caches the app shell + WASM modules via the service worker. After that, the PWA loads from cache regardless of connectivity. The actual file processing is in-browser anyway — files never leave the device — so offline use is a first-class case.

View file

@ -66,7 +66,7 @@ Email, USB stick, Signal/AirDrop/Magic Wormhole — anything that moves a single
- **No lazy-loading.** The PDF strategy (~430 KB) loads eagerly even if the user never processes a PDF. For the hand-off use case this is fine; for a hosted deploy it would be wasteful.
- **No service worker.** The deployed PWA caches itself for offline use after first visit; the standalone HTML is *always* offline because it never had a network connection to lose.
- **No PWA install.** "Add to home screen" requires a service worker. Users who want the app icon experience should install from the hosted PWA instead.
- **No PWA install.** "Add to home screen" requires a service worker. Android users who want a real app icon should install the [Android APK](android-apk.md); users with a self-hosted PWA deploy can install from there.
- **No favicon.** Stripped because `<link rel="icon">` would trigger a CORS error in some browsers' console under `file://`. Cosmetic, no functional impact.
- **No manifest.** Same reason.
- **Bigger initial load.** 830 KB single file vs ~620 KB across two split chunks. ~30% size penalty for the convenience of a single deliverable.