Skip to content

Fix/2026 09 13/audit response round1 - #41

Merged
janakhpon merged 17 commits into
mainfrom
fix/2026-09-13/audit-response-round1
Sep 13, 2026
Merged

janakhpon merged 17 commits into
mainfrom
fix/2026-09-13/audit-response-round1

Conversation

@janakhpon

Copy link
Copy Markdown
Member

What changed

Why

How it was verified

  • pnpm --filter ./apps/web test
  • pnpm --filter ./apps/web run lint
  • cd services/feedback && go vet ./... && go test ./... (if Go changed)
  • Android unit tests, if Android changed: cd apps/android && ./gradlew testDebugUnitTest
    (needs JAVA_HOME on Android Studio's JBR; see apps/android/README.md).
    CI does not run them, so this is the only gate on that suite.
  • iOS MonOcrCore tests, if iOS changed: cd apps/ios && sh Scripts/swift-test.sh.
    The ios-core job runs these on every push, so this is a faster copy of a
    gate that does exist. The app target is still not built anywhere.

Claims

.material-symbols-outlined only set font-variation-settings; nothing ever
told it to actually use the Material Symbols font. The Google Fonts link in
+layout.svelte was loading correctly the whole time -- an earlier read of
this bug (mine included) wrongly assumed a CDN-timing/FOUC race. It isn't:
this is deterministic and reproduces on every load, on any connection,
which is what four independent role-based audits converged on after
checking the actual rule in app.css rather than guessing from the symptom.

Confirmed in the compiled output, not just the source: built CSS now reads
font-family:Material Symbols Outlined on the rule.

Also removed docs/+page.svelte's local scoped override of the same
selector -- it never set font-family either, so it was already dead
(the global rule's !important on font-variation-settings dominated it),
and keeping it only would have invited a future edit to 'fix' a rule with
no visible effect either way.

Verified: type-check clean (1507 files, 0 errors), lint clean, vitest
172/172, build succeeds, and the fix confirmed present in the actual built
CSS asset -- not just inferred from source.
Each modal's Escape handler lived on the backdrop div -- a sibling, not an
ancestor, of the focus-trapped content. Once focusTrap moves focus inside
on open (which it always does), a keydown on the backdrop can never fire:
events bubble through ancestors only.

Fixed once, in focus-trap.ts itself, via an optional onEscape callback
wired into the action -- every consumer inherits correct behavior from one
place instead of re-implementing (and mis-wiring) it per modal.

Also added role="dialog" aria-modal="true" aria-labelledby to all three
modals (ConfirmationModal, SuccessModal, HistorySection's record viewer),
and aria-hidden="true" on their decorative icons -- screen readers
previously got no announcement that a dialog opened, or what it was
called, and would speak raw icon-ligature text next to labels that already
state the same thing.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
Design-system root causes:
- shadow-huge was never a defined Tailwind utility -- all four floating
  surfaces (2 modals, the history record panel, LanguageToggle's dropdown)
  silently rendered with zero elevation. Replaced with shadow-xl
  everywhere it was used. HistorySection's record panel additionally had
  no border at all as a fallback -- added one, matching the other modals.
- .btn-primary was missing the border .btn-secondary carries, causing a
  1px size mismatch between adjacent primary/secondary buttons. Added a
  transparent border of the same width so both share one box model.
- Neither button class had any disabled: styling -- a disabled submit
  button rendered pixel-identical to an enabled one. Added
  disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
  to both, at the one shared definition instead of per call site.
- Added a real .btn-compact modifier and migrated +page.svelte's three
  toolbar buttons onto it. They previously layered `px-3 py-1` on top of
  .btn-primary/.btn-secondary, hoping to override the padding -- but those
  classes are @apply'd in the same Tailwind layer and land later in the
  compiled output, so at equal specificity they always won; the override
  never took effect. Verified against the compiled CSS before and after.
- Header's scrolled-state translucency used bg-opacity-90/dark:bg-opacity-90,
  both removed in Tailwind v4. Confirmed zero rule emitted for either
  class in the compiled output. Replaced with a computed bg-canvas/90;
  confirmed the real color-mix rule now compiles.

Keyboard gap:
- +page.svelte's dropzone (role="button") only handled Enter, not Space --
  the conventional key for activating a button role, and one this same
  codebase's own ActionBox.svelte already gets right. Added the Space
  branch with preventDefault so it doesn't also scroll the page.

Contrast fixes, computed from the actual hex/opacity values, not estimated:
- --accent-gold (2.85:1 on light canvas) darkened to #7a6200 (5.68:1) for
  light mode only -- the original #b2940a already passes in dark mode
  (6.19:1) and is restored there.
- --fg-muted (3.33:1 on light canvas -- fails even before any opacity is
  layered on top) darkened to #6b6b6d (5.14:1 canvas / 4.83:1 subtle).
  Dark mode's #8e8e93 already passed and is untouched.
- .section-label, .zen-label, .zen-code-comment, Header's version string,
  and +page.svelte's dropzone subtitle and word-count line all stacked an
  opacity utility on top of an already-marginal color -- opacity alone
  defeats any base-color fix, so each had the opacity dropped rather than
  chasing it with an ever-darker base.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds;
spot-checked the compiled CSS for shadow-xl, .btn-compact, bg-canvas/90's
color-mix rule, and the corrected token values.
Docs page's left-nav section headers and right-side TOC header both used
text-fg-primary/40 (~2.3:1) -- these are primary in-page navigation
labels, not decoration. Its breadcrumb nav used text-fg-muted/60. Privacy
page's footer date line and the report page's consent-checkbox helper
text both used opacity-60/70 on top of an already-muted color. All five
dropped the opacity; the first two also moved to the (now AA-passing)
fg-muted token instead of fg-primary at reduced strength.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
The generated manifest (vite.config.ts's SvelteKitPWA config, what
actually ships -- it and a hand-authored static/manifest.webmanifest
collide at the same build path, and the generated one wins) referenced
/android-chrome-192x192.png and /android-chrome-512x512.png. Neither file
exists anywhere in the repo; only web-app-manifest-192x192.png and
web-app-manifest-512x512.png do, confirmed at the exact declared
dimensions (192x192, 512x512) via sips. This blocks or degrades Chrome's
install-to-homescreen prompt, which needs a real >=192px icon.

Beyond the icon paths, the shipping config had drifted from the correct,
never-served static/manifest.webmanifest in several ways: an indigo
theme_color (#4338ca) that matches nothing else in the app, instead of
#7a1b1b -- the actual value in app.html's own <meta name="theme-color">;
a white background_color instead of #fcfbf9, this app's real --bg-canvas;
a generic description instead of the real one; the name 'MonOCR Web'
instead of 'MonOCR'; and literal, never-customized SvelteKit starter-
template categories (['template', 'starter', 'sveltekit']) instead of
this app's actual categories. Brought the shipping manifest in line with
the correct content (also added the missing screenshots entry and the
96x96 icon), then deleted the now-fully-redundant static file -- it never
shipped, and keeping two sources of truth is exactly what let this drift.

Verified: build succeeds, and the built manifest.webmanifest inspected
directly for the corrected name/description/colors/categories/icons.
og-image.jpg is 1024x1024 (verified via sips), not the 1200x630 the
screenshots entry claimed -- carried over uninspected from the dead
static/manifest.webmanifest in the prior commit, whose own stated
verification only covered icon dimensions, not this. Caught by an
independent verifier. Also dropped form_factor: 'wide' -- a square image
is neither wide nor narrow, and a wrong hint is worse than none.
- Footer.svelte (rendered on every page, not scoped to one route) had two
  lines at opacity-60/40 on top of the inherited text-fg-secondary --
  worse than several of the ones already fixed (2.66:1 and 1.85:1), and
  global rather than page-scoped. Missed in the prior sweep entirely.
- docs/+page.svelte's own sweep was incomplete: 6 more identical-pattern
  labels (code-block language tags, two section h3s) still had
  opacity-40 on inherited text, computing to the same ~2.3:1 the already-
  fixed ones did. Moved to text-fg-muted (now AA-passing) instead.
- contribute/+page.svelte's "OR" divider label inherited opacity-30 from
  its wrapping container -- meant only for the divider line, but CSS
  opacity cascades to all descendants, crushing the actual text to
  ~1.49:1. Moved the opacity onto each border line individually instead.

Left alone, checked and not failing: LanguageToggle's Mon-heritage label
(opacity-70 on fg-accent computes to 4.52:1, a genuine if marginal AA
pass); the breadcrumb "/" and bullet-point dividers (decorative
punctuation, not content); the two docs-page copy-button icons
(non-text UI glyphs); Breadcrumb.svelte's own opacity-50 label (same
bug, but the component is dead code, never rendered anywhere).

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
- HistorySection.svelte's 'Retry' link (real actionable text on a failed
  sync record) sat at opacity-60 against its row background, computing to
  4.49:1 idle / 4.46:1 on hover -- just under the 4.5:1 AA line in the
  opposite direction from a boundary case checked earlier this branch.
  Removed the opacity (and the now-meaningless hover:opacity-100/
  transition-opacity that existed only to compensate for it).
- contribute/+page.svelte's Mon-script textarea and report/+page.svelte's
  corrected-text textarea both used placeholder:text-fg-muted/30 and /40
  -- computing to ~1.49:1 and ~1.72:1. Same opacity-stacked-muted-text bug
  already fixed elsewhere on this branch, just on the placeholder:
  variant, which the earlier greps for plain opacity-NN and non-placeholder
  text-*/NN patterns didn't catch.

Also corrects a false claim in the immediately prior commit's message: it
said contribute's OR-divider had 'an existing asymmetry' between its two
border lines. Checked full file history this time -- there wasn't one;
both lines always shared one wrapping div's opacity equally. The fix
itself (splitting the opacity onto each line individually) was still
correct, only that one sentence describing prior state was wrong.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
Removed the Mon-heritage subtitle under the language name in
LanguageToggle's dropdown (all three locales), per direct request. The
message key (nav_mon_heritage) is now unreferenced anywhere in src, so
removed it from all three locale files too, and regenerated paraglide's
compiled output to match. Left about_vulnerable untouched -- a distinct,
still-used message describing the UNESCO classification on the About
content, not this dropdown label.

Verified: type-check clean, lint clean (also dropped LanguageToggles
now-unused m import), vitest 172/172, build succeeds.
Removed the hardcoded contributors list (3 names, roles, and external
profile links) from the docs page, along with its left-nav/TOC entries
and the two message keys that became unreferenced as a result
(docs_governance_contributors, docs_contributors_title), then regenerated
paraglide to match. Footer prev/next navigation is hardcoded to fixed
routes, not derived from the nav arrays, so removing this entry does not
affect it.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
Section 7 (Global Compliance) had **GDPR**/**CCPA** typed as literal
Markdown-style asterisks directly in the Svelte template rather than
<strong> tags -- this repo has no Markdown rendering pipeline, so users
saw the raw asterisks on screen, on a legal/compliance page. Flagged by
an earlier audit, not actually fixed until now -- confirmed live via
Playwright against the dev server at /privacy (rendered body text
contained a literal '**' before the fix, does not after).

Also confirmed, while checking: the page's two <footer> elements (the
shared layout Footer and this page's own effective-date footer, nested
inside <main>) are not an accessibility bug -- only one is exposed as the
contentinfo landmark, which is correct per the HTML5 spec (a footer
nested in a sectioning root gets no implicit landmark role).

Verified: type-check clean, lint clean, vitest 172/172, build succeeds,
and the live dev server re-checked directly.
Footer read 'MonOCR Version 0.2.0' as a literal string; the real
package.json version is 0.4.0 -- two minors stale. Header.svelte already
solved this exact problem with a vite __APP_VERSION__ define injected
from package.json at build time (itself fixing a prior '1.0.0 vs 0.3.0'
drift, per its own comment); this page just never got the same fix.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds.
Every icon in the app now renders through one Icon.svelte component
(~20 call sites migrated) instead of a ligature web font loaded from a
Google Fonts CDN. This is the root architectural cause behind the
literal-icon-name-as-text production bug fixed earlier this branch --
the ligature approach has no fallback when the font is slow, blocked, or
(as it turned out) missing font-family entirely; an inline SVG has no
font/network dependency and no text-fallback failure mode. Matches the
pattern this app already used correctly for the header hamburger, theme
toggle, and language toggle icons -- and the direction multiple design
audits converged on independently.

Icon names keep each old Material Symbols identifier (upload_file,
check_circle, etc.) as their lookup key, so Badge.svelte's icon prop and
every dynamic {fileType.includes('pdf') ? ... : ...} call site needed no
rewiring beyond the wrapper element.

Removed as dead weight once the migration was complete:
- The Google Fonts <link> for Material Symbols, and the CSS rule that
  loaded it (+layout.svelte, app.css) -- along with the unused
  .animate-spin-slow keyframe sitting next to it.
- A second Google Fonts <link> for Public Sans and its --font-public-sans
  token, confirmed applied nowhere in the codebase (same dead-font
  pattern already found and fixed on the sibling portfolio.zinmin.2025
  site).
- Three fully-dead components (Button.svelte, Breadcrumb.svelte,
  Dropzone.svelte) -- each exported from the barrel, none imported
  anywhere; every real call site already hand-rolled its own equivalent
  inline instead (and had already drifted from what the shared
  component did).

Also, while migrating docs/+page.svelte's copy-code buttons: fixed their
hover-only visibility (invisible and undiscoverable on touch devices) and
32px hit area (under the 44px minimum), and added the same copy affordance
to the CLI reference block, which had none at all despite being the
section most worth copying from.

Verified: type-check clean (0 errors), lint clean, vitest 172/172, build
succeeds -- and live-checked with Playwright against the built preview
server: icons render as real SVG glyphs (not text), zero Google Fonts
requests fire, zero console errors.
Migrated the two remaining material-symbols-outlined spans to Icon.
Separately, per direct request: the 104px gap between the Error Type
pills and the consent checkbox (mb-16 + pt-10) read as dead space --
tightened to mb-16 + pt-6. The Share Correction / Cancel Feedback pair
was two equally-heavy full-width buttons stacked vertically; Cancel is
now a lighter text-only action beside the primary button instead of
competing with it for visual weight, matching this session's compact,
GitHub-inspired direction rather than the site's original mega-button
pattern.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds,
and visually re-checked via Playwright screenshot.
Per direct request, and applied consistently across the whole app, not
just the one screenshotted instance: GitHub's own buttons size to their
content, they don't stretch to fill their container. HistorySection's
own confirm dialog already did this correctly (flex justify-end +
min-w-[100px]) -- every other stretched button in the app was the
exception, not the rule.

- ConfirmationModal: Cancel/Confirm were flex-1 (50/50 split); now
  right-aligned, content-sized, matching HistorySection's own dialog.
- SuccessModal: the solitary Done button was w-full; now centered and
  content-sized (the modal's own content is text-center, so this reads
  as one deliberate choice, not a stray override).
- report/+page.svelte: Share Correction was flex-1, forcing it to fill
  whatever space Cancel didn't take. Now min-w-[140px], sized to its
  own content next to Cancel, not to the row.
- contribute/+page.svelte: Submit Contribution was w-full inside a
  centered max-w-md wrapper; now min-w-[160px], left-aligned with the
  rest of the page's content instead of centered on its own.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds,
and visually re-checked both pages via Playwright screenshot.
Found by the verifier checking the previous button-sizing commit:
ConfirmationModal's confirm button carried border-red-500/bg-red-500 as
plain utility classes on top of .btn-primary, but .btn-primary's own
@apply'd background is in the same Tailwind layer and wins the cascade
regardless of markup order -- the same issue already documented for
.btn-compact. The button rendered near-black, undercutting the one
visual signal that a confirm action (clearing all history) is
irreversible.

Fixed with a real .btn-danger modifier, defined after .btn-primary in
the same layer so it wins instead of losing -- confirmed in the compiled
CSS (byte offset after .btn-primary) and by computed style in a live
browser (renders Tailwind's actual red-500, not black).

Also restored a responsive fallback the button-sizing fix had dropped:
the footer was flex-col+sm:flex-row (stacks full-width on narrow
screens, right-aligned content-sized on wider ones); the sizing fix had
collapsed it to always-row. Full width only below the sm breakpoint,
content-sized above it -- keeps a headroom margin for whatever a future
label or locale needs, per the verifier's note.

Verified: type-check clean, lint clean, vitest 172/172, build succeeds;
compiled-CSS cascade order and live computed background-color both
directly confirmed, not inferred.
New file -- this repo had no prior benchmark.md. Covers apps/web's 16
commits (icon-rendering root cause, the full SVG icon migration, modal
accessibility, four rounds of contrast fixes, two rounds of PWA manifest
fixes, content-scope removals, content-accuracy fixes, button
consistency, a destructive-button color fix), the verification discipline
behind them (7 adversarial passes, 4 of which found something real),
explicit known gaps not yet acted on, and the fresh apps/android and
apps/ios audits (2 platforms, real blocking findings each, no fixes
applied yet).
@janakhpon
janakhpon merged commit 162f7e4 into main Sep 13, 2026
17 checks passed
@janakhpon
janakhpon deleted the fix/2026-09-13/audit-response-round1 branch September 13, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant