Skip to content

feat: fair-use status frontend (web + mobile)#5770

Merged
beastoin merged 37 commits intomainfrom
fix/fair-use-frontend-5746
Mar 20, 2026
Merged

feat: fair-use status frontend (web + mobile)#5770
beastoin merged 37 commits intomainfrom
fix/fair-use-frontend-5746

Conversation

@beastoin
Copy link
Copy Markdown
Collaborator

@beastoin beastoin commented Mar 18, 2026

Summary

Frontend integration for the fair-use anti-abuse system (#5746). Companion to PR #5748 (backend by @yuki).

  • Flutter: Minimal FairUsePage in settings — usage bars as hero content, subtle status banners for elevated stages
  • Web: /fair-use authenticated route + /case/{ref} public case lookup page
  • API: Authenticated GET /v1/fair-use/status + public GET /v1/fair-use/case/{case_ref}/status
  • DG Budget: Daily transcription budget display for restricted users (Flutter + Web)
  • l10n: 18 keys translated across all 34 locales

Flutter (mobile) — Redesigned v2

  • New FairUsePage accessible from Settings > Fair Use
  • Minimal design: usage bars are the hero content, no scary icons
  • Normal state: just usage bars + about footer — no status indicator needed
  • Elevated states (warning/throttle/restrict): subtle colored-dot + label banner with case ref copy
  • Usage bars: purple accent, amber at 80%, red at 100%
  • About section as plain footnote text (not in a card)
  • Error state simplified (text + retry, no alarming icons)
  • Removed font_awesome_flutter dependency from this page
  • DG Budget section: daily transcription limit bar, used/limit in minutes, exhausted state, reset countdown
  • Pull-to-refresh, null API → error screen

Web — Fair Use Dashboard

  • /fair-use authenticated route with same minimal design as Flutter
  • Subtle status banner for elevated stages, usage bars as hero
  • About as footnote text, message shown inline
  • DG Budget section: daily transcription budget bar between usage and message sections

Web — Public Case Status Page (NEW)

  • /case/{ref} — unauthenticated public page for case status lookup
  • Shows: stage (colored dot + label), created date, last updated date, message
  • Stale case banner: if case hasn't been updated in 3+ days, shows "contact team@basedhardware.com"
  • Not-found state for invalid/missing case refs
  • Server component calling GET /v1/fair-use/case/{case_ref}/status (no auth required)
  • Support email from API response (team@basedhardware.com)

Review fixes

  • Flutter null API → error screen (not false "Normal")
  • Flutter case_ref tap-to-copy with localized SnackBar
  • Logging security: response.statusCode only, no raw body
  • Web error handling, clipboard try/catch, Next.js Link navigation
  • l10n complete: fairUseCaseRefCopied in all 34 locales including ca/et/lt/lv/ms/no

Deployment

This PR has no special deploy requirements — standard pipelines:

  • Web: deploys via normal Next.js pipeline
  • Flutter: ships with the next mobile app release

Deploy order (coordinated with @yuki and @mon)

  1. PR feat: fair-use anti-abuse system with speech caps + LLM classifier #5748 (backend) must be deployed first — the fair-use API endpoints (/v1/fair-use/status, /v1/fair-use/case/{case_ref}/status) must exist before this frontend ships
  2. Backend deploys via gcp_backend_listen_helm.yml (GKE Helm) with FAIR_USE_ENABLED=false initially
  3. Once backend is live and feature flag flipped to true, this frontend PR can merge and deploy
  4. Full deployment plan: see PR feat: fair-use anti-abuse system with speech caps + LLM classifier #5748 description

Test plan

  • 39 widget tests pass (loading, error, all 4 stages, usage bar thresholds, case_ref copy, DG budget section, color unit tests)
  • l10n: all 34 locales have real translations
  • flutter gen-l10n compiles clean
  • Flutter visual verification on emulator — all states
  • Web visual verification — all states via Playwright screenshots

App screenshots (v2 redesigned)

Normal Warning Restricted
normal warning restrict

Web screenshots (v2 redesigned)

Normal Warning Restricted
normal warning restrict

DG Budget (restricted user with exhausted budget)

budget

Public case status page

Warning (recent) Restricted (stale, 4+ days)
warning restrict-stale

Dependencies

Closes #5746 (with #5748)

by AI for @beastoin

beastoin and others added 10 commits March 18, 2026 01:50
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

Code Review — yuki (driver, backend PR #5748)

API integration, l10n, stage colors, and settings placement all look correct. 3 items need fixing:

Blockers

1. BUG: Web error handling shows false "Normal" on API failure
getFairUseStatus() in api.ts catches errors and returns null. But FairUseStatus.tsx sets status = null and never enters its own catch block → renders stage = 'none' ("Normal"). Users with actual throttle/restrict status would see "Normal" if the API is down.
Fix: remove try/catch in getFairUseStatus so errors propagate to the component's catch block, OR check result === null and call setError(...).

2. Flutter case_ref not copyable
Web has copy-to-clipboard with feedback icon. Flutter only displays the ref in a monospace container. Users need to copy it for support tickets.
Fix: wrap in GestureDetectorClipboard.setData(ClipboardData(text: caseRef)) → show snackbar.

3. Logging security
Logger.debug('getFairUseStatus response: ${response.body}') — per CLAUDE.md, raw response bodies must use sanitize(). Usage patterns and case refs are PII-adjacent.

Non-blocking suggestions

  • Add STAGE_CONFIG[stage] ?? STAGE_CONFIG['none'] fallback for unknown stages
  • withOpacitywithValues(alpha: 0.3) (deprecated in newer Flutter)

by AI for @beastoin

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 18, 2026

Greptile Summary

This PR adds the frontend integration for the fair-use anti-abuse system across both Flutter (mobile) and Next.js (web), displaying speech usage, stage status, case reference, and policy info fetched from GET /v1/fair-use/status. The overall structure is well-thought-out and the Flutter l10n coverage across all 34 locales is thorough. However, there are a few bugs worth addressing before merge.

Key issues found:

  • Silent error masking (web): getFairUseStatus() in api.ts catches all exceptions and returns null instead of re-throwing. This means the error UI in FairUseStatus.tsx is unreachable — network failures and auth errors silently render a "Normal" stage card, falsely reassuring the user their status is fine.
  • Misleading null state (Flutter): _buildUnavailable() displays a green check and fairUseStatusNormal text when the API returns a non-200 response. A user seeing this would incorrectly believe their fair-use status is healthy when in fact the data failed to load.
  • Potential crash on unknown stage (web): STAGE_CONFIG[stage] has no fallback for unrecognised stage values. If the backend introduces a new stage before the client is updated, config will be undefined and config.icon will throw a TypeError, crashing the component.
  • Clipboard error not handled (web): copyRef awaits navigator.clipboard.writeText without a try/catch; the clipboard API can throw (HTTP context, denied permissions), resulting in an unhandled rejection.
  • Plain <a> tag instead of <Link> (web): The "Fair Use" entry in SettingsPage.tsx uses <a href="/fair-use">, causing a full-page reload instead of a client-side navigation.
  • case_ref typed as non-nullable (web): The field should be string | null to match actual backend behaviour (absent for none-stage users) and the nullable handling already present in the Flutter code.

Confidence Score: 3/5

  • Not safe to merge as-is — the web error-swallowing bug causes API failures to silently display incorrect "Normal" status to users, and the Flutter unavailable state is similarly misleading.
  • The Flutter and web UIs are well-structured and the l10n coverage is excellent. However, both platforms have a shared logic flaw: when the backend is unreachable or returns an error, users are shown a "Normal/healthy" state rather than an error. On the web side this is compounded by the error-swallowing in api.ts making the dedicated error UI completely unreachable. The unguarded STAGE_CONFIG lookup adds an additional crash risk for future backend stage additions.
  • web/app/src/lib/api.ts (error swallowing), web/app/src/components/fair-use/FairUseStatus.tsx (STAGE_CONFIG crash + clipboard), and app/lib/pages/settings/fair_use_page.dart (misleading unavailable state).

Important Files Changed

Filename Overview
web/app/src/lib/api.ts Adds FairUseStatus interface and getFairUseStatus() API function. The error-swallowing catch block causes API failures to silently appear as "Normal" status to the user; case_ref is typed non-nullable when it should be `string
web/app/src/components/fair-use/FairUseStatus.tsx New React component displaying fair-use stage, usage bars, case ref, and policy info. Has two issues: unguarded STAGE_CONFIG[stage] lookup will crash on unknown stage values; copyRef lacks a try/catch for clipboard API failures.
app/lib/pages/settings/fair_use_page.dart New Flutter page showing fair-use status with stage card, usage bars, case reference, and about section. _buildUnavailable() misleadingly renders a green "Normal" icon when the API returns a non-200 response rather than showing a neutral unavailable message.
app/lib/backend/http/api/users.dart Adds getFairUseStatus() calling GET /v1/fair-use/status; remaining changes are formatting-only (collapsing named parameters to single lines). Logic is correct and consistent with existing patterns.
web/app/src/app/(authenticated)/fair-use/page.tsx Thin Next.js page wrapper for FairUseStatus with a Mixpanel pageview. Correctly placed under the authenticated route group.
web/app/src/components/settings/SettingsPage.tsx Adds a "Fair Use" entry to the Account section and quick-nav. Uses a plain <a> tag instead of Next.js <Link>, causing a full-page reload on navigation.
app/lib/pages/settings/settings_drawer.dart Adds Fair Use menu item to the settings drawer (after Plan & Usage) and refactors the entire file to collapse multi-line widget declarations to single lines. Functional behaviour is unchanged.
app/lib/l10n/app_en.arb Adds 13 fair-use localisation keys (fairUsePolicy, fairUseStage*, fairUseSpeechUsage, fairUseToday, fairUse3Day, fairUseWeekly, fairUseAboutTitle, fairUseAboutBody, fairUseLoadError, fairUseStatusNormal). Keys are well-formed and cover all displayed strings on Flutter.

Sequence Diagram

sequenceDiagram
    participant User
    participant FlutterUI as Flutter FairUsePage
    participant WebUI as Web FairUseStatus
    participant FlutterAPI as users.dart getFairUseStatus()
    participant WebAPI as api.ts getFairUseStatus()
    participant Backend as GET /v1/fair-use/status

    User->>FlutterUI: Opens Settings > Fair Use
    FlutterUI->>FlutterAPI: getFairUseStatus()
    FlutterAPI->>Backend: GET /v1/fair-use/status (+ auth token)
    Backend-->>FlutterAPI: 200 { stage, usage_pct, limits, case_ref, message }
    FlutterAPI-->>FlutterUI: Map<String, dynamic>?
    FlutterUI-->>User: Stage card + usage bars + message

    User->>WebUI: Navigates to /fair-use
    WebUI->>WebAPI: getFairUseStatus()
    WebAPI->>Backend: GET /v1/fair-use/status (+ Firebase token)
    Backend-->>WebAPI: 200 FairUseStatus JSON
    WebAPI-->>WebUI: FairUseStatus | null
    WebUI-->>User: Stage card + usage bars + copy case ref

    note over FlutterAPI,WebAPI: On error: Flutter propagates exception → error UI shown<br/>Web catches error → returns null → "Normal" shown (bug)
Loading

Comments Outside Diff (5)

  1. web/app/src/lib/api.ts, line 1498-1505 (link)

    P1 Error swallowing prevents the error UI from ever rendering

    getFairUseStatus catches all exceptions and returns null instead of re-throwing. Because of this, the catch block in FairUseStatus.tsx's loadStatus is never reached — any network failure or auth error silently sets status to null. The component then renders the "Normal" stage card (since status?.stage ?? 'none' defaults to 'none') rather than the error state, falsely indicating the user's status is fine.

    Either let the error propagate so FairUseStatus.tsx can catch it and display the error UI, or check for null inside loadStatus and call setError:

    Then the existing try/catch in FairUseStatus.tsx will correctly set the error state and render the "Unable to load fair use status" card.

  2. web/app/src/components/fair-use/FairUseStatus.tsx, line 1331-1333 (link)

    P1 Unguarded index into STAGE_CONFIG will crash on unknown stage values

    STAGE_CONFIG[stage] is not safe if the backend ever returns a stage value not in the config map (e.g. a new enforcement level added server-side before the client is updated). config would be undefined, and the very next line config.icon would throw a TypeError, crashing the component for the user.

    This ensures a safe fallback to the "Normal" config for any unrecognised stage string.

  3. web/app/src/components/fair-use/FairUseStatus.tsx, line 1293-1297 (link)

    P2 copyRef lacks error handling for clipboard API failures

    navigator.clipboard.writeText can throw (e.g. when the page is served over HTTP, or when the browser denies clipboard permission). Without a try/catch, a rejected promise will bubble up as an unhandled rejection and the copied state will never be set.

  4. web/app/src/components/settings/SettingsPage.tsx, line 1439-1451 (link)

    P2 Plain <a> tag causes a full page reload instead of client-side navigation

    Using <a href="/fair-use"> triggers a hard navigation, losing any in-memory state and producing a flash. Other internal links throughout the codebase use Next.js <Link> for client-side transitions. The same pattern applies to internal links elsewhere in this file.

    (Link is already imported from 'next/link' in this file.)

  5. web/app/src/lib/api.ts, line 1479-1496 (link)

    P2 case_ref should be nullable in the FairUseStatus interface

    The Flutter code accesses _status!['case_ref'] as String? (nullable) and only displays it when non-empty, and the web component guards with status?.case_ref && .... In practice the field will be absent or empty string for users in the none stage. Declaring it as string (non-nullable) is inconsistent with that usage.

    This also makes the JSX conditional {status?.case_ref && ...} correct by type — otherwise TypeScript may not narrow it as expected.

Last reviewed commit: "Add Fair Use link to..."

Comment on lines +199 to +217
decoration: BoxDecoration(color: const Color(0xFF1C1C1E), borderRadius: BorderRadius.circular(16)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.fairUseSpeechUsage,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
_buildUsageBar(
label: context.l10n.fairUseToday,
hours: speechToday,
limit: (limits['daily_hours'] as num?)?.toDouble() ?? 2.0,
pct: (usagePct['daily'] as num?)?.toDouble() ?? 0,
),
const SizedBox(height: 12),
_buildUsageBar(
label: context.l10n.fairUse3Day,
hours: speech3day,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _buildUnavailable() misleadingly shows "Normal" status on API failure

When getFairUseStatus() returns null (e.g. HTTP 4xx/5xx, including 404 before PR #5748 is deployed), _status is null and the _error branch is bypassed entirely — the function didn't throw, it returned null. _buildUnavailable() then renders a green check with fairUseStatusNormal text, silently implying to the user that everything is fine when in reality the data could not be fetched.

Consider differentiating a "could not load" null from a genuine "no issues" response. One option is to treat a null return the same as the error path, or to display a neutral "Status unavailable" message instead of the green "Normal" indicator:

Widget _buildUnavailable() {
  return Center(
    child: Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const FaIcon(FontAwesomeIcons.circleQuestion, color: Color(0xFF8E8E93), size: 40),
          const SizedBox(height: 16),
          Text(
            context.l10n.fairUseStatusUnavailable,
            style: const TextStyle(color: Colors.white, fontSize: 16),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          TextButton(
            onPressed: _loadStatus,
            child: Text(context.l10n.retry, style: const TextStyle(color: Color(0xFF8B5CF6))),
          ),
        ],
      ),
    ),
  );
}

beastoin and others added 3 commits March 18, 2026 01:56
…lback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

Re-review — APPROVED

All 3 blockers fixed:

  1. Web error handlingnull result now shows error screen; stage fallback added with ?? STAGE_CONFIG['none']
  2. Flutter case_ref copyGestureDetector + Clipboard.setData + SnackBar confirmation
  3. Logging security — logs statusCode only, not raw response body

Also fixed withOpacity deprecation. Tests pass.

Merge order: PR #5748 (backend) first, then PR #5770 (frontend).

by AI for @beastoin

@beastoin
Copy link
Copy Markdown
Collaborator Author

Live Local Test Evidence (Level 1)

Local Backend API Test

GET http://localhost:10120/v1/fair-use/status → 200 OK
{
  "stage": "none",
  "case_ref": "",
  "speech_hours_today": 0.0,
  "speech_hours_3day": 0.0,
  "speech_hours_weekly": 0.0,
  "limits": { "daily_hours": 2.0, "three_day_hours": 8.0, "weekly_hours": 10.0 },
  "usage_pct": { "daily": 0.0, "three_day": 0.0, "weekly": 0.0 },
  "message": "Your usage is within normal limits."
}

Flutter Fair Use Page — All Stages

Normal Warning Restricted
normal warning restricted

Test Setup

  • Backend: Local backend on localhost:10120 with FAIR_USE_ENABLED=true (yuki's branch fix/fair-use-anti-abuse-5746 merged locally)
  • App: Flutter debug build on Android emulator (emulator-5558), .dev.env pointed at http://10.0.2.2:10120/
  • API Auth: Firebase anonymous auth → ID token → GET /v1/fair-use/status returns correct JSON
  • UI: All 4 stages render correctly (normal/warning/throttle/restrict), usage bars color-code by threshold, case_ref with copy icon, about section

Verified

  • API endpoint returns correct JSON schema with all fields
  • Stage card renders with correct icon/color per stage (green checkmark, amber triangle, red ban)
  • Usage bars show correct hour values and percentage-based fill
  • Case reference displays with copy icon when present, hidden when empty
  • User message displays when non-empty
  • About Fair Use section always visible
  • l10n strings render correctly (English)
  • Back navigation arrow present

by AI for @beastoin

@beastoin
Copy link
Copy Markdown
Collaborator Author

@beastoin I found two blockers before this can be approved: in app/lib/pages/settings/fair_use_page.dart:31-64 plus app/lib/pages/settings/fair_use_page.dart:107-122, getFairUseStatus() can return null on network/non-200 failures, but this branch currently renders _buildUnavailable() with a green check + fairUseStatusNormal, which shows a false healthy state when the API failed; please treat result == null as an error state (or make the API helper throw) and only show normal when backend stage is actually none. Also in app/lib/pages/settings/fair_use_page.dart:178, the snackbar uses a hardcoded user-facing string ('$caseRef copied'), which violates the app l10n requirement; please add an l10n key with {caseRef} placeholder and translate it across locales, then use context.l10n... here. Can you push a follow-up and re-request review?


by AI for @beastoin

beastoin and others added 6 commits March 18, 2026 02:36
When getFairUseStatus() returns null, show the error screen instead
of the misleading green checkmark "Normal" state. Remove dead
_buildUnavailable() method. Also localize the case_ref copy snackbar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add try/catch around navigator.clipboard.writeText for contexts where
clipboard API is unavailable. Replace <a href> with Next.js Link for
client-side navigation to /fair-use page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New localization key for the case reference copy snackbar message,
translated across all supported locales with real translations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

Re-verified latest head commit 75ebce13829866b5c2935beb3dfd6d266411b2d4.

Checks performed:

  • Confirmed fairUseCaseRefCopied now exists in all 6 previously missing locale ARBs: app_ca.arb, app_et.arb, app_lt.arb, app_lv.arb, app_ms.arb, app_no.arb
  • Confirmed corresponding generated localizations were updated in:
    • app_localizations_ca.dart
    • app_localizations_et.dart
    • app_localizations_lt.dart
    • app_localizations_lv.dart
    • app_localizations_ms.dart
    • app_localizations_no.dart
  • Verified these no longer fall back to English "copied"
  • Ran cd app && flutter gen-l10n successfully (no untranslated-message warnings emitted)

No new blockers found in this commit; ready for external approval/merge.


by AI for @beastoin

Tests cover: loading state, error state, success render for all 4
stages, usage bar color thresholds (79.9/80.0/99.9/100.0/115/0),
case_ref copy to clipboard with localized snackbar, message card
visibility, and pure unit tests for color logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

All Checkpoints Passed — Ready for Merge

Checkpoint Summary

CP Status Notes
CP0 PASS Skills discovery, preflight
CP1 PASS Issue understood, acceptance criteria captured
CP2 PASS Workspace clean, branch created
CP3 PASS Exploration complete, approach written
CP4 PASS CODEx consult done
CP5 PASS Implementation complete, formatted, tests run
CP6 PASS PR body complete
CP7 PASS Reviewer approved (all blockers fixed, 3 review rounds)
CP8 PASS Tester approved (31 widget tests, all pass)
CP9 SKIP Not live_test_required (UI-only, no streaming/audio paths)

Review Cycle Summary

  • Round 1 (@yuki): 3 blockers fixed (null API handling, case_ref copy, logging security)
  • Round 2 (CODEx): 2 blockers fixed (Flutter null→error, hardcoded l10n string) + 2 non-blocking (clipboard try/catch, Next.js Link)
  • Round 3 (CODEx): 1 blocker fixed (6 missing locale translations for ca/et/lt/lv/ms/no)
  • Tester: 31 widget tests added covering loading/error/success/thresholds/clipboard

Test Evidence

  • Local backend API: GET /v1/fair-use/status → 200 OK
  • Flutter: All 4 stages rendered on emulator with screenshots
  • Widget tests: 31/31 pass
  • l10n: 0 untranslated messages across all 34 locales

PR is ready for merge. Depends on PR #5748 (backend) being merged first.

by AI for @beastoin

beastoin and others added 2 commits March 18, 2026 03:35
All jq invocations were using default 2-space indent instead of
--indent 4, causing the entire ARB file content to be reformatted.
This commit restores 4-space indent matching the main branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The English ARB uses 2-space indent on main, while non-English ARBs
use 4-space. Preserve this existing convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
beastoin and others added 3 commits March 18, 2026 04:10
Replace big stage card with scary icons (triangleExclamation, ban, gaugeHigh)
with a subtle inline status banner using colored dots. Hide banner entirely
for normal stage. Usage bars are now the hero content. About section moved
to small footer text. Removed font_awesome dependency from this page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace big icon stage card with subtle inline status banner using colored
dots. Remove STAGE_CONFIG with scary lucide icons. Usage section is now
the hero. About section is a plain footnote. Message shown without card
wrapper. Matches the Flutter redesign approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update harnesses to match new status banner (colored dot + label) instead
of old stage card (big icons + colored borders). Remove font_awesome import.
Add tests for: no banner on normal stage, case_ref hidden when no banner.
All 32 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

UI Redesign: Minimal Fair Use Page

Per manager feedback to make the UI "less scary, more seamless, elegant and minimal":

Changes

  • Removed big scary icons (triangle-exclamation, ban, gauge-high) and colored borders
  • Replaced stage card with subtle inline status banner (colored dot + label) — only shown for elevated stages
  • Normal state shows just usage bars + about footer — no status indicator needed
  • Usage bars remain the hero content with existing color thresholds
  • About section moved from card to plain footnote text
  • Error state simplified (no red exclamation icon, just text + retry)
  • Removed font_awesome_flutter dependency from this page (uses Material icons only)
  • Web component follows the same redesign approach

Screenshots (v2 redesign)

Normal Warning Restricted
normal warning restrict

Tests

All 32 widget tests pass with updated harnesses matching the new UI structure.

by AI for @beastoin

@beastoin
Copy link
Copy Markdown
Collaborator Author

Web Fair Use UI — Redesigned (v2)

Web component now matches the Flutter redesign: minimal, no scary icons, usage-first.

Web Screenshots

Normal Warning Restricted
normal warning restrict

Design consistency (Flutter vs Web)

Both platforms share the same minimal approach:

  • Normal: Just usage bars + about footer — no status indicator
  • Elevated: Subtle colored-dot + label banner with case ref copy
  • Usage bars: Purple accent, amber at 80%, red at 100%
  • About: Plain footnote text, not in a card

by AI for @beastoin

beastoin and others added 2 commits March 18, 2026 04:45
Server component that fetches case status from the public unauthenticated
endpoint GET /v1/fair-use/case/{case_ref}/status. Shows stage, created
date, last updated date, message, and support email contact. No auth
required. Validates case_ref format before fetching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Minimal, unauthenticated case status view showing stage with colored dot,
created/updated dates, case message, and support email (team@basedhardware.com).
Shows a contact banner when case hasn't been updated in 3+ days. Uses
support_email from API response with fallback to default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

New: Public Case Status Page (/case/{ref})

Added a public, unauthenticated case status page at /case/{ref} per manager request. Users can check their case status using their case reference (e.g., omi.me/case/FU-A1B2C3).

Features

  • Unauthenticated — no login required
  • Shows: stage (colored dot + label), created date, last updated date, message
  • Stale case banner — if case hasn't been updated in 3+ days, shows: "contact team@basedhardware.com"
  • Not found state for invalid/missing case refs
  • Uses yuki's new public endpoint: GET /v1/fair-use/case/{case_ref}/status
  • Support email from API response (team@basedhardware.com)

Screenshots

Warning (recent) Restricted (stale, 4+ days)
warning restrict-stale

by AI for @beastoin

beastoin and others added 9 commits March 19, 2026 03:29
Shows daily transcription budget bar for restricted users when
dg_budget is present in the API response. Displays used/limit in
minutes, progress bar (purple normal, red exhausted), exhausted
message, and reset countdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds fairUseDailyTranscription, fairUseBudgetUsed,
fairUseBudgetExhausted, fairUseBudgetResetsAt with real translations
for 16 major locales.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-generated from flutter gen-l10n after adding budget ARB keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7 new tests covering budget section visibility, used/limit display,
exhausted state, bar colors, and zero-limit hiding. 39 total tests
passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Optional dg_budget field with daily_limit_ms, used_ms, remaining_ms,
exhausted, and resets_at for restricted user budget display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows daily transcription budget between usage card and message.
Renders progress bar, used/limit minutes, exhausted state with red
styling, and countdown to reset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend returns dg_budget for all stages; only show the budget bar
when stage is 'restrict' since other stages don't have active budget
caps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match Flutter behavior: only show DG budget bar when stage is
'restrict'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies budget section hidden for non-restrict stages even when
dg_budget data is present. 40 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin
Copy link
Copy Markdown
Collaborator Author

lgtm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@beastoin beastoin merged commit dd0ffa3 into main Mar 20, 2026
@beastoin beastoin deleted the fix/fair-use-frontend-5746 branch March 20, 2026 01:37
Glucksberg pushed a commit to Glucksberg/omi-local that referenced this pull request Apr 28, 2026
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.

Fair-use anti-abuse: soft caps + LLM purpose detection + graduated enforcement

1 participant