Commit graph

496 commits

Author SHA1 Message Date
Randa
1b0896f264 docs(security): mark CSP + deps remediation items resolved (PR #196, #197)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 52s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 1m10s
CI / E2E (Standalone single-file) (push) Successful in 1m55s
CI / E2E (Web) (push) Successful in 4m1s
Updates the 2026-05-22 audit to reflect the three shipped fixes:
- #2 yarn audit CI gate (PR #196)
- #3 devDependency vulnerabilities 42 → 0 (PR #196)
- #4 style-src 'unsafe-inline' removed from all three CSP layers (PR #197)

New score: 72/82 (was 66/82). Sole remaining HIGH: .env keystore password.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:09:07 +04:00
1c642f5b37 security: remove style-src 'unsafe-inline' from all CSP policies (#197)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 36s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 1m2s
CI / E2E (Standalone single-file) (push) Successful in 1m55s
CI / E2E (Web) (push) Has been cancelled
Closes #193

Migrate three inline-style React props to CSS classes / CSSOM:
- ErrorExpansion: cursor:copy and copy-hint color moved to BEM classes
- SegmentedControl: dynamic transform now driven by --ec-segment-offset CSS var, set via useLayoutEffect + ref.style.setProperty

Remove 'unsafe-inline' from style-src in all three enforcement layers:
- vite.config.web.ts (prod only; dev keeps it for HMR)
- nginx.conf (both CSP directives)
- public/_headers (Cloudflare Pages)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:06:28 +04:00
4efdd770dd feat(diff): copy to clipboard markdown (#187) (#195)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 36s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 1m12s
CI / E2E (Standalone single-file) (push) Successful in 2m52s
CI / E2E (Web) (push) Successful in 4m36s
2026-05-22 23:54:59 +04:00
e35f8aad2d security: upgrade deps + add audit CI gate (#191 #192) (#196)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 41s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 52s
CI / E2E (Standalone single-file) (push) Successful in 1m46s
CI / E2E (Web) (push) Successful in 3m39s
Upgrades @capacitor/cli to ^7.6.5 and vite to ^7.3.3, adds yarn resolutions to force patched transitive versions for minimist, ansi-regex, rollup, picomatch, postcss and brace-expansion. Reduces audit findings from 42 (2 critical, 21 high, 19 moderate) to zero. Wires yarn audit --level high into CI test job.
2026-05-22 22:48:43 +04:00
c249ec86ea fix(android): deliver cleaned files on Android APK (#186) (#189)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 32s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 45s
CI / E2E (Standalone single-file) (push) Successful in 1m46s
CI / E2E (Web) (push) Successful in 3m25s
Fixes silent file-delivery failure on Android Capacitor APK. Cleaned files now save via SAF (user picks location) and share via native intent (FileProvider-wrapped URI).

- Custom Capacitor plugin (SaveToDownloadsPlugin.java) with saveAs() (SAF) and share() (ACTION_SEND).
- FileProvider configured in AndroidManifest + res/xml/file_paths.xml.
- BrowserFileBytes writes cleaned bytes to app-private cache; URI stashed for Save/Share.
- Per-row Save + Share icons; per-batch Save zip + Share zip buttons.
- outputPath added to FileEntry to thread the cleaned-output path through to the UI.
- @capacitor/filesystem ^7 added; @capacitor/share intentionally not used (rejects content:// URIs).

Closes #186
2026-05-22 22:19:11 +04:00
de2aded598 docs(security): full security audit report 2026-05-22 (#194)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 30s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 45s
CI / E2E (Standalone single-file) (push) Successful in 1m31s
CI / E2E (Web) (push) Successful in 3m32s
Scored 66/82. High: plaintext keystore password in .env. Medium: dev-dep vulns (minimist critical, ansi-regex high), no SCA in CI, style-src unsafe-inline in all CSP layers.
2026-05-22 22:02:19 +04:00
Randa
0345162c46 docs(plan): Android downloads fix implementation plan (#186)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 31s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 46s
CI / E2E (Standalone single-file) (push) Successful in 1m32s
CI / E2E (Web) (push) Successful in 3m30s
2026-05-22 21:16:03 +04:00
Randa
a2936d89ac docs(spec): Android downloads fix — @capacitor/filesystem + per-file share (#186)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:16:03 +04:00
d9e763b76e feat(zip): generic ZIP support with recursive inner-file cleaning (#184) (#188)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 32s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 45s
CI / E2E (Standalone single-file) (push) Successful in 1m35s
CI / E2E (Web) (push) Successful in 3m23s
2026-05-22 20:32:03 +04:00
a5546afa71 feat(wasm): FfmpegFallbackStrategy for MP4/MOV/M4V/MKV/WebM (#183)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 29s
CI / Smoke build (VITE_ENABLE_FFMPEG_FALLBACK=false) (push) Successful in 44s
CI / E2E (Standalone single-file) (push) Successful in 1m33s
CI / E2E (Web) (push) Successful in 3m24s
Adds FfmpegFallbackStrategy as a peer to ExifToolFallbackStrategy, routing MP4/MOV/M4V (Phase 1) and MKV/WebM (Phase 2) through @ffmpeg/core. On by default for all three distributions (standalone HTML, Capacitor APK, PWA self-host); VITE_ENABLE_FFMPEG_FALLBACK=false opts out. Takes priority over VideoStrategy for the MP4 family; VideoStrategy stays registered as the opt-out fallback until a subsequent PR deletes it.

Closes #182. Closes #43.

Resolves the four documented walker KNOWN_GAPS categorically: handler-name leak (#38), compressor-name leak (#39), mvhd.next_track_id leak (#111), GPMF/GPS coordinates leak (#42). On gopro-fusion.mp4 (5.1 MB GPMF + tmcd + fdsc) and dji-phantom4.mov (236 MB UserData GPS log) the forensic battery reports zero device-fingerprint survival across every recovery technique.

Key architectural choices:

- **Main-thread @ffmpeg/core, not @ffmpeg/ffmpeg wrapper.** The wrapper hardcodes type:"module" Workers from Blob URLs, which fail silently under null-origin file:// in Chromium — the standalone build hung forever on every video strip. @ffmpeg/ffmpeg dropped from package.json.
- **Stream mapping -map 0 -map -0:d? -map -0:s? -map -0:t?**. Preserves input track order while dropping data/subtitle/timecode streams. Avoids the eng→und reorder bug of -map 0:v?/-map 0:a?, and sidesteps mat2's exit-234 on action-cam files (GoPro Fusion has tmcd/fdsc).
- **Post-strip pass rewrites the udta box type to 'free'** (ISO/IEC 14496-12 §8.1.2 padding) to neutralise ffmpeg's hardcoded HandlerType:Metadata + HandlerVendorID:Apple stub. Length-preserving so stco/co64 offsets stay valid. Handles both regular and largesize headers via headerStart+4.
- **mdhd.language left as ffmpeg's 'und'** — considered zeroing but reverted: 0x0000 is an invalid ISO 639-2/T code, ffprobe falls back to displaying '(eng)' for invalid codes (actively misleading downstream tools).
- **Diff race fix.** @uswriting/exiftool's parseMetadata uses module-level singletons (Perl, MemoryFS, stdout/stderr StringBuilders). WasmProcessor now serializes all diff builds across the processor's lifetime via a Promise chain — guarantees no two parseMetadata calls overlap, whether within an entry or across the fire-and-forget chunk-drained queue.
- **ExifTool family-1 group names surfaced verbatim** — IFD0, ExifIFD, XMP-dc, Track1, etc. Refuses to collapse to umbrella labels like 'EXIF' because the collapse caused (source, name) key collisions across sub-groups (Track1:HandlerType vs Track2:HandlerType produced spurious diffs on multi-track MP4).
- **Standalone HTML stays single-file.** Two-asset Vite plugin gzip+base64-inlines ffmpeg-core.js + ffmpeg-core.wasm into <script type=text/plain> tags, mirroring the zeroperl pattern. With tree-shaking via __WITH_STANDALONE_INLINE__ the standalone HTML went 116MB → 24MB.

Forensic verification: docs/forensic/ffmpeg-fallback.md + tools/forensic/ffmpeg-fallback.ts cover synthetic-mp4/mkv/webm + phone-baseline (2.7MB Android) + gopro-fusion (5MB action-cam) + dji-phantom4 (236MB drone) with zero sentinel/fingerprint survival across the recovery battery. Gap analyses for all three formats at docs/gap-analysis/mp4-ffmpeg.md, mkv.md, webm.md. POC at docs/poc/ffmpeg-wasm.md.

Production deps go from 5 → 6: @ffmpeg/core@0.12.10 (GPL-2.0-or-later; combined distributable inherits, MIT codebase unchanged, source pointer in README per GPL compliance).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:04:04 +04:00
b2dec037a8 feat(android): assembleRelease + env-var signing config (#165) (#185)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 30s
CI / E2E (Standalone single-file) (push) Successful in 1m27s
CI / E2E (Web) (push) Successful in 2m43s
Local-only Android release signing, env-var driven. The maintainer keeps the .jks on their machine; CI age-encrypted-in-repo variant deferred to a follow-up. Adds signingConfigs.release reading 4 env vars (v1+v2+v3 enabled), scripts/build-apk-local.sh --release with .env auto-sourcing and --env-file secret handling, .env.sample template, root .gitignore for *.jks/*.keystore, peer-promoted Signed release APK section in docs/android-apk.md, and new docs/android-signing.md with the keystore ceremony, threat model, backup checklist, and rotation procedure.

Closes #165.
2026-05-22 12:14:01 +04:00
e3f630a913 fix(apk): safe-area-inset-top for Android status bar (#180) (#181)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 32s
CI / E2E (Standalone single-file) (push) Successful in 1m25s
CI / E2E (Web) (push) Successful in 2m53s
Capacitor 7 enables edge-to-edge on Android by default, so the WebView extends behind the system status bar. Add viewport-fit=cover to the meta viewport tag and apply env(safe-area-inset-top, 0px) to .app and .settings-drawer so content clears the clock/battery area.

Desktop/web fallback is 0px — zero visual change on non-Android targets.
2026-05-21 19:18:03 +04:00
8853dbc49e refactor(diff): prune legacy MetadataItem; integrate Office/PDF walker entries (chunk B.1) (#179)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 31s
CI / E2E (Standalone single-file) (push) Successful in 1m26s
CI / E2E (Web) (push) Successful in 2m54s
Closes #178. Chunk B.1 of #174.

Removes the legacy per-strategy MetadataItem[] enumeration from JPEG/PNG/MP4/fallback (ExifTool read covers them richer than our walkers ever did), while keeping Office and PDF walker emission plumbed through a new walkerEntries: MetadataEntry[] channel on StripResult.

Plus two perf fixes:
- Standalone WASM stashed in <script type='text/plain'> instead of inlining the 25 MB Base64 as a module-scope JS literal (~500-1500ms page-load saved)
- Deferred + chunked diff drain so directory drops don't compete with strip CPU on the main thread; cap-eviction regression fixed so all entries get their diff regardless of batch size
2026-05-21 18:45:04 +04:00
aed20576e9 docs(spec): diff pivot to full before/after via ExifTool (#176)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 32s
CI / E2E (Standalone single-file) (push) Successful in 1m39s
CI / E2E (Web) (push) Successful in 2m54s
Approved spec for chunk B (PR #177).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:27:36 +04:00
158de36044 feat(diff): ExifTool-sourced two-pane before/after metadata diff (#177)
Some checks failed
CI / E2E (Web) (push) Blocked by required conditions
CI / E2E (Standalone single-file) (push) Blocked by required conditions
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Chunk B of #174. Pivots the diff view (#22) from per-strategy delta to a unified ExifTool dump on both source + stripped bytes, displayed in a diffchecker-style two-pane layout.

Refs #22 #174 #176

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:27:20 +04:00
e7e4af2282 feat(wasm): ExifToolFallbackStrategy skeleton (#175)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 29s
CI / E2E (Standalone single-file) (push) Successful in 1m11s
CI / E2E (Web) (push) Successful in 3m14s
Lands chunk A of #174: ExifTool-in-WASM fallback strategy. Pins @uswriting/exiftool@1.0.9 + @6over3/zeroperl-ts@1.0.10, claims .webp/.gif/.avif after per-format forensic verification (zero sentinel survival), and wires the strategy last in strategy_registry.ts behind the VITE_ENABLE_EXIFTOOL_FALLBACK build flag (default on; escape hatch only).

Browser bundling: Vite emits zeroperl.wasm via a ?url asset import + dynamic-import-inside-typeof-window guard so vitest/tsx don't try to resolve the ?url suffix. The 25 MB WASM is served via runtimeCaching (CacheFirst) — fetched on first WebP/GIF/AVIF drop and cached forever after; JPEG users never trigger the fetch.

Forensic harness adopts KNOWN_GAPS / UNVERIFIABLE discipline from tools/forensic/video.ts. Surfaced TIFF leaking IFD0 tags (ExifTool limitation; not claimed) and BMP being unwriteable by ExifTool (not claimed). HEIC/HEIF deferred to chunk C (fixture builder requires libheif).

No-network proof: zero sock_* imports in zeroperl.wasm; static grep finds no JS network primitives; dynamic verification under `node --permission` (no --allow-net) passes.

Refs #174 (does not close — chunks B-F still ahead).
2026-05-21 12:05:08 +04:00
b5eccc6f55 feat(standalone): inline manifest + favicon as data URLs (#173)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 1m12s
CI / E2E (Web) (push) Successful in 2m20s
2026-05-21 02:34:54 +04:00
5d17fa0cb9 docs(poc): WebPerl + ExifTool WASM evaluation
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 1m13s
CI / E2E (Web) (push) Successful in 2m26s
POC of @uswriting/exiftool + @6over3/zeroperl-ts as an opt-in path to RAW/HEIC/AVIF coverage on the standalone + APK targets.

Verified zero outbound network capability (no sock_* in the WASM imports). Sentinel-survival = 0 on JPEG/PNG/MP4/AVIF; PDF write blocked by zeroperl's missing dynamic Perl module loading; ~900 ms/file perf so the engine is on-demand only.

Recommendation lands in docs/poc/webperl-exiftool.md: two thin strategies (ExifToolFallbackStrategy + ExifToolDiffStrategy) sharing one lazy-loaded engine, opt-in via build flag for standalone HTML + Android APK targets.

Implementation tracked in #174.
2026-05-21 02:31:40 +04:00
6e52fd894f 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
2026-05-21 02:27:41 +04:00
Randa
e8c0940024 docs(spec): hide folder picker on mobile (#164)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 1m13s
CI / E2E (Web) (push) Successful in 2m16s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:27:52 +04:00
3b997cab04 fix(empty-state + file-table): hide folder picker on mobile (#164); widen RESULT col for CI font fallback (#168)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Has been cancelled
CI / E2E (Web) (push) Has been cancelled
Fixes #164.

Two small fixes bundled:

- EmptyState: hide FolderPickButton on touch-primary devices. The button's underlying input uses webkitdirectory, unsupported on mobile browsers — tapping it falls back to a plain file picker. New shouldRenderFolderPickButton() helper gates the component on isMobileDevice(). Unit + e2e coverage included.

- file_table.css: widen the phone-width RESULT column 100->110px. The 100px budget shipped in #167 was tuned against macOS font rendering; CI's Chromium uses a different system-ui fallback that renders 'Ya estaba limpio' at ~106px, tipping the file_list_layout.spec.ts regression test over.
2026-05-21 00:27:01 +04:00
181e22a7ff fix(file-table): phone-width column layout + error wrap + layout regression test (#167)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Successful in 1m23s
CI / E2E (Web) (push) Failing after 2m8s
Fixes #102.

- Phone-width @media block shrinks STATUS/TYPE/BEFORE/AFTER/RESULT to fit iPhone 14 (390px), with the name column getting a readable share via minmax(0, 1fr).
- .file-table__error-text gets white-space: pre-wrap + overflow-wrap: anywhere so the <pre> wraps inside the row instead of clipping at the right edge.
- .file-table__cell .type-pill padding tightened to 4px horizontal so DOCX (58px on WebKit) fits the 56px TYPE column.
- .file-table__reveal hidden at phone widths (reveal.showInFolder is a no-op in browsers anyway).
- New e2e regression test parametrised over JPG/DOCX with phase-2 inject of "Ya estaba limpio" (longest result text in the i18n bundle).
2026-05-20 23:12:42 +04:00
9c99c0b43c feat(android-local): vendored Docker build for APK with no host SDK (#163)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 29s
CI / E2E (Standalone single-file) (push) Successful in 1m13s
CI / E2E (Web) (push) Successful in 2m12s
Adds a self-contained local APK build path. Only Docker is required on the host — no JDK, no Android SDK, no Node. Mirrors .github/workflows/build-android.yml inside a vendored image so anyone who clones the repo can build the APK with one command.

- docker/android-builder/Dockerfile — node:22-bookworm-slim (pinned by digest) + Temurin JDK 21 + Android SDK 35 + build-tools 35.0.0 and 34.0.0 (AGP 8.7.x's silent internal minimum). JAVA_HOME symlinked via dpkg --print-architecture so the image works on amd64 and arm64.
- scripts/build-apk-local.sh — orchestrator that builds the image on first run, then runs the container as the host uid:gid with the repo bind-mounted and three repo-local cache dirs. Uses --network host (Docker Desktop 4.34+ on macOS/Windows) to dodge a default-bridge apt-download failure.
- docs/android-apk.md — splits Prerequisites into Docker (Path A, recommended) and Native (Path B), adds a Local Docker build section.
- .gitignore — adds .docker-cache/.
2026-05-20 19:10:27 +04:00
33a75c2493 test(video-forensic): mat2 column + real-world fixtures + expanded sentinels (#108 #135) (#162)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 1m17s
CI / E2E (Web) (push) Successful in 2m10s
2026-05-20 19:04:27 +04:00
bca9203216 fix(ci-android): pin upload-artifact to v3 for Forgejo 10 v1-API (#161)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 1m37s
CI / E2E (Web) (push) Successful in 2m44s
Forgejo 10.0.3 only implements the v1 artifact API. v4 forks (both upstream and forgejo/) speak v2. Pin to actions/upload-artifact@v3. Bump when stack moves to Forgejo 11+.
2026-05-17 17:36:14 +04:00
dfdbbc73f6 fix(ci-android): use Forgejo fork of upload-artifact (#160)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Successful in 1m18s
CI / E2E (Web) (push) Successful in 2m35s
Hotfix for run #165. actions/upload-artifact@v4 uses @actions/artifact v2+ which rejects non-github.com origins as GHESNotSupportedError. Pin to https://code.forgejo.org/forgejo/upload-artifact@v4.
2026-05-17 17:15:34 +04:00
a5ff315ebd fix(ci-android): bump fallback setup-java to JDK 21 (#159)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 1m24s
CI / E2E (Web) (push) Successful in 2m17s
Companion to forgejo-stack#3. Capacitor 7.6 requires JDK 21; matches the fallback path to the prebaked-image path.
2026-05-17 16:42:25 +04:00
9019739d01 fix(ci-android): pin android-actions/setup-android to https://github.com/ (#158)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 31s
CI / E2E (Standalone single-file) (push) Successful in 1m29s
CI / E2E (Web) (push) Successful in 2m32s
Hotfix for run #159. Forgejo runner clones every uses: reference at workflow-prep time, even for steps gated by if:. data.forgejo.org mirrors actions/* but not android-actions/*, so the clone 404s and the workflow fails before any step runs. Inline https://github.com/ URL bypasses the Forgejo mirror.
2026-05-17 16:27:20 +04:00
e68a8141bf feat(android): Capacitor APK wrapper + on-demand CI build (#156)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Successful in 1m13s
CI / E2E (Web) (push) Successful in 2m21s
Closes #153.

- Capacitor v7 scaffold producing a sideloadable debug APK from dist/web/
- .github/workflows/build-android.yml — workflow_dispatch only; soft dependency on forgejo-stack/job-android via use_prebaked_image input (fast path ~3-5 min, fallback installs JDK + Android SDK at runtime)
- Privacy hardening: allowBackup=false, FileProvider removed, Google Services plugin reference removed, usesCleartextTraffic=false, dataExtractionRules deny-all
- INTERNET permission documented in docs/PRIVACY_GAPS.md (Capacitor requires it; CSP connect-src 'self' enforces no traffic)
- Branded launcher icons + splash regenerated from .resources/icon.png
- docs/android-apk.md updated with Q1-Q5 decisions + CI section
- docs/PRIVACY_GAPS.md updated with Android <=9 Downloads/ world-readable gap
- Plan: docs/superpowers/plans/2026-05-17-android-apk-and-ci.md

Q1-Q5 decisions:
- Q1 file output: accept Downloads/ (documented privacy gap on Android <=9)
- Q2 back button: Capacitor default (close on root)
- Q3 android/ placement: master
- Q4 CI: in scope, workflow_dispatch only
- Q5 min API: 23 (Capacitor 7 default)

Review rounds 1 and 2 closed all P0 + P1 findings. Three P2 polish items split to follow-ups.
2026-05-17 16:15:22 +04:00
204c49cb76 test(png-forensic): add mat2 comparative column (#133) (#157)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 1m12s
CI / E2E (Web) (push) Successful in 2m16s
2026-05-17 16:12:06 +04:00
9400371f78 test(jpeg-forensic): add mat2 comparative column (#132) (#155)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Successful in 1m16s
CI / E2E (Web) (push) Successful in 2m20s
2026-05-17 16:05:06 +04:00
2d2613628c docs(android): expand APK guide + add AAR feasibility analysis (#154)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 31s
CI / E2E (Standalone single-file) (push) Successful in 1m5s
CI / E2E (Web) (push) Successful in 2m14s
Multi-agent research pass (Android expert, PWA/mobile expert, TechLead).

Expands docs/android-apk.md: Capacitor v7 version pins, config gotchas
(server.androidScheme, wasm-unsafe-eval CSP), .gitignore for android/,
<a download> privacy exposure on Android 9, WebView vintage per platform,
cap sync failure mode, F-Droid sub-section, signed APK workflow, splash
screen note. Adds full AAR feasibility analysis (6 approaches evaluated;
verdict: wont do). Closes research phase of #153.
2026-05-17 13:47:26 +04:00
52196e08c3 refactor(strategies): extract diff enumeration into sibling files for PNG and JPEG (#152)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 32s
CI / E2E (Web) (push) Has been cancelled
CI / E2E (Standalone single-file) (push) Has been cancelled
2026-05-17 13:45:42 +04:00
be19ae6582 feat(i18n): Arabic as default language + translate Name/Type columns (#151)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 27s
CI / E2E (Standalone single-file) (push) Successful in 1m40s
CI / E2E (Web) (push) Successful in 2m52s
- Set Arabic as the default language for new users
- Add columnName/columnType i18n keys (English + Arabic)
- Wire FileTable.tsx headers to use t() instead of hardcoded strings
- Seed English in launchPage() so e2e specs keep asserting English strings
- Update settings_schema tests to expect ar default
2026-05-16 17:18:39 +04:00
9e40f72d48 fix(office): use meta:name attr for user-defined props; guard parseOfficeProperties throws (#141) (#150)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 54s
CI / E2E (Web) (push) Successful in 1m59s
Two follow-up fixes from the phase3-office-hardening review:

1. ODT meta:user-defined properties now use the meta:name attribute as the display name rather than localName, which collapsed multiple distinct properties into identical rows.

2. Added try/catch around all three parseOfficeProperties call sites in stripOoxml and stripOdt so an unexpected throw cannot escalate to an invalid-file-format error.

Closes #141.
2026-05-16 16:42:33 +04:00
c820941f59 refactor(video): extract diff enumeration into video_diff.ts (#146) (#149)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 29s
CI / E2E (Standalone single-file) (push) Successful in 1m5s
CI / E2E (Web) (push) Successful in 2m16s
Split video_strategy.ts into video_boxes.ts (parsing primitives), video_diff.ts (read-only enumeration), and video_strategy.ts (strip policy only). No behaviour change.
2026-05-16 16:35:12 +04:00
cccefaa865 fix(status-bar): count only cleaned files, show rejected segment (#101) (#148)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 30s
CI / E2E (Standalone single-file) (push) Successful in 59s
CI / E2E (Web) (push) Has been cancelled
Extracts file-stat computation into useFileStats hook. cleanedCount = Complete + NoMetadataFound, errorCount = Error. StatusBar now renders an N rejected segment when errors are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:33:16 +04:00
e857500d87 feat(i18n): complete Spanish and Arabic translations (#147)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Web) (push) Has been cancelled
CI / E2E (Standalone single-file) (push) Has been cancelled
47 es keys + 44 ar keys filled — diff panel, metadata groups, status bar, offline indicator, theme picker, error messages, privacy hint.
2026-05-16 16:32:04 +04:00
d389cba4f4 feat(diff): Video metadata diff enumeration (#125) (#145)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 54s
CI / E2E (Web) (push) Successful in 1m59s
Populate metadataItems in VideoStrategy.strip() for all three metadata sources:

- Timestamp boxes (mvhd/tkhd/mdhd): emit modified items with QT date before/after epoch
- udta tags: walk ilst before blanking, emit removed items per iTunes tag
- uuid XMP box: emit removed XMP item with byte-size summary

Adds parseBoxesSafe() so malformed subtrees never abort the strip.
No strip policy changes.
2026-05-16 16:16:21 +04:00
9b0d7e4c8e feat(diff): emit structural removal items in Office metadata diff (#143)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 55s
CI / E2E (Web) (push) Successful in 2m1s
2026-05-16 16:13:09 +04:00
444ba67668 fix(status-bar): wire up tags-removed count (#144)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 24s
CI / E2E (Standalone single-file) (push) Successful in 2m23s
CI / E2E (Web) (push) Successful in 3m27s
## Summary

- `totalTagsRemoved` in `App.tsx` was hardcoded to `undefined` (a TODO placeholder left before the diff feature shipped), causing the status bar to always show "0 tags removed"
- Replace with a `reduce` over `state.files`, summing `metadataItems` entries where `action !== "kept"` — the same filter `use_process_files` already uses per file
- No new logic: data was fully populated end-to-end through every strategy → `WasmProcessor` → `AppContext`; only the read in `App.tsx` was missing

## Test plan

- [ ] Drop a JPEG/PNG/PDF/DOCX and confirm the count in the status bar is non-zero after processing
- [ ] All 481 unit tests pass (`yarn test`)
- [ ] `yarn typecheck` clean

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

Co-authored-by: Randa <obuvuyoviz26@gmail.com>
Reviewed-on: http://localhost:3000/forgejo_admin/exifcleaner-web/pulls/144
2026-05-16 15:45:27 +04:00
398728924b test(pdf-forensic): add mat2 comparative column (#140)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 1m25s
CI / E2E (Web) (push) Successful in 2m32s
Closes #134. Mirrors PR #129 for `tools/forensic/office.ts`.

## Summary

- `tools/forensic/pdf.ts` now invokes mat2 (Poppler/Cairo) as a side-by-side reference alongside ExifTool and Ghostscript. Skip-if-missing locally; mat2 stays out of CI per the convention established in #129.
- New report fields: strip-tool `Producer` fingerprints, `/Info CreationDate` / `ModDate` stamps, and a rasterised-page-content flag (any `/Subtype /Image` XObject in the output).
- The fingerprint sweep runs over `qpdf --qdf` decompressed dump **and** a raw zlib brute-force pass over the input bytes — mat2 hides `/Info` inside a stream qpdf flags with an `unexpected xref entry type` warning and silently omits from the qdf output, so the brute-force pass is what surfaces its `cairo X.Y.Z` Producer + current `CreationDate`.
- `docs/forensic/pdf.md` gains the "Comparison reference: mat2" methodology subsection, mat2 columns in the results + per-sentinel tables, footnoted explanations of why mat2's strip is total (rasterisation), and an interpretation paragraph naming the structural differences.

## Findings vs the issue's open questions

- **XMP / annotations / etc.** — mat2 drops every original indirect object as a side effect of the Poppler/Cairo rewrite. 0 sentinels survive any of the 5 recovery channels.
- **Own ModDate / Producer fingerprint** — mat2 stamps `Producer = cairo 1.18.0 (https://cairographics.org)` and a fresh `/Info CreationDate` set to the current time. `ModDate` is absent. `PdfStrategy` writes none of these.
- **Rasterisation** — confirmed. mat2's output contains `/Subtype /Image` XObjects per page (category difference); the other three tools preserve vector/text content. Documented as the headline tradeoff in the writeup.

On this fixture, `PdfStrategy` and mat2 are equivalent for sentinel survival (0 each). No `docs/gap-analysis/pdf.md` update needed — there's no divergence to note.

## Test plan

- [x] `npx tsx tools/forensic/pdf.ts` → `PdfStrategy` column reports 0 leaked across all 5 recovery channels; mat2 column populated; comparison table prints clean
- [x] `yarn typecheck`
- [x] `yarn test tests/infrastructure/wasm/pdf_strategy.test.ts` (15/15)
- [x] `yarn test tests/infrastructure/wasm/` (244/244)
- [x] `yarn lint`
- [x] `yarn check:deps`

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

Co-authored-by: Randa <obuvuyoviz26@gmail.com>
Reviewed-on: http://localhost:3000/forgejo_admin/exifcleaner-web/pulls/140
2026-05-16 15:18:57 +04:00
ec5c9159d8 feat(diff): Office metadata diff enumeration (#124) (#137)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 26s
CI / E2E (Web) (push) Has been cancelled
CI / E2E (Standalone single-file) (push) Has been cancelled
2026-05-16 15:17:19 +04:00
8b66a1fb56 fix(diff): show "Cleaned" when bytes reduced but no named metadata items (#142)
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 26s
CI / E2E (Standalone single-file) (push) Successful in 1m25s
CI / E2E (Web) (push) Has been cancelled
## Summary

- Files with structural-only cleanup (Office ZIP compaction, Content_Types orphan removal) shrank in size but showed "Already clean" because `metadataItems` was empty
- Fix: also treat `outputBytes < entry.size` as a cleaned result, so any genuine byte reduction surfaces as "Cleaned" regardless of whether named items were found
- Applies to all formats — not Office-specific

## Test plan

- [ ] Drop a DOCX with no author/title/company fields — should now show "Cleaned" with the correct before/after sizes instead of "Already clean"
- [ ] Drop a DOCX with metadata fields — still shows "Cleaned" with named items in the diff
- [ ] Drop a file that was already stripped (no size change, no items) — still shows "Already clean"
- [ ] `yarn test` passes (444 tests)

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

Co-authored-by: Randa <obuvuyoviz26@gmail.com>
Reviewed-on: http://localhost:3000/forgejo_admin/exifcleaner-web/pulls/142
2026-05-16 15:15:16 +04:00
9b33ba3c6d feat(office): Phase 3 hardening — Tracks A+B+C.2 + structural-fingerprint cleanup (#129)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 25s
CI / E2E (Standalone single-file) (push) Successful in 2m3s
CI / E2E (Web) (push) Successful in 3m10s
Closes #121.

Phase 3 metadata-strip hardening (Tracks A.1-A.3, B.1-B.2, C.2) plus a 2026-05-16 structural-fingerprint cleanup pass surfaced during the mat2 comparison review.

- Track A.1: docProps/thumbnail.png deletion
- Track A.2: w14:paraId/w14:textId attr stripping
- Track A.3: header/footer/footnotes/endnotes inline track-change + rsid stripping
- Track B.1: <w14:docId>/<w15:docId> persistent GUID removal in settings.xml
- Track B.2: <w:mailMerge> data-source block removal
- Track C.2: word/vbaProject.bin deletion
- Structural-fingerprint cleanup: empty ZIP directory entries dropped (compactZip rebuild with createFolders:false), docProps/custom.xml deleted outright, orphan <Override> entries pulled from [Content_Types].xml, unused xmlns:w14/w15 dropped from settings.xml + body parts, non-root empty <Relationships> wrappers deleted, blank-line runs collapsed.

Forensic comparison vs mat2 (the FOSS reference): we leak 4 fewer sentinels (RELS_FILEPATH, PARA_ID_ATTR, DOC_GUID, MAIL_MERGE_PATH); tie on structural fingerprints (3 each, all deliberate Office-conformance shells).
2026-05-16 12:38:51 +04:00
193b575a60 fix(png-jpeg-diff): defend against parseXmpPacket throw (#138)
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 29s
CI / E2E (Standalone single-file) (push) Successful in 1m6s
CI / E2E (Web) (push) Successful in 2m18s
2026-05-16 11:45:12 +04:00
731c684352 feat(diff): PDF metadata diff enumeration (#122) (#127)
Some checks failed
CI / E2E (Web) (push) Blocked by required conditions
CI / E2E (Standalone single-file) (push) Blocked by required conditions
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
2026-05-16 11:45:05 +04:00
1e610c1def chore: remove ci-trigger artifact from cache verification
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 26s
CI / E2E (Standalone single-file) (push) Successful in 56s
CI / E2E (Web) (push) Successful in 2m1s
2026-05-16 11:33:27 +04:00
7d72d0218b chore: revert yarn.lock auto-refresh marker
All checks were successful
CI / Lint, Typecheck & Unit Tests (push) Successful in 28s
CI / E2E (Standalone single-file) (push) Successful in 2m36s
CI / E2E (Web) (push) Successful in 3m44s
2026-05-16 11:21:24 +04:00
973d4ebcc5 chore: nudge yarn.lock to verify auto-refresh
Some checks failed
CI / Lint, Typecheck & Unit Tests (push) Successful in 18s
CI / E2E (Standalone single-file) (push) Has been cancelled
CI / E2E (Web) (push) Has been cancelled
2026-05-16 11:19:20 +04:00