diff --git a/.claude/skills/ato-website-architecture/SKILL.md b/.claude/skills/ato-website-architecture/SKILL.md new file mode 100644 index 0000000..8a19d48 --- /dev/null +++ b/.claude/skills/ato-website-architecture/SKILL.md @@ -0,0 +1,221 @@ +--- +name: ato-website-architecture +description: Use when navigating or modifying lib/ in the Anaheim Technologies website, adding or wiring a route/page, working with the GoRouter ShellRoute + HomeScreen chrome, adding a bloc/cubit or reading state flow, touching the responsive Constants.is*Screen static-globals pattern, editing MasterDetailScreen / shared widget_utils backgrounds, dealing with web-only Firebase init, or tracing the contact-form Firestore write on the website side. +--- + +# Anaheim Technologies website — architecture + +How the Flutter web app is assembled: entrypoints, routing, state, responsive layout, +shared widgets, Firebase init, and the contact-form data flow. Repo root (all paths below +are relative to it): +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website`. + +Web-only Flutter app (Very Good CLI scaffold) → Firebase Hosting, single project +`anaheim-technologies`. Firestore is the only backend in this repo; the email side lives in +a **separate repo** (see the contact-form section). + +## When NOT to use this skill (use the sibling instead) + +| You are doing… | Use skill | +|---|---| +| Toolchain (fvm 3.41.6), build/run commands, CI/CD, how changes ship | `ato-website-build-deploy` | +| Marketing copy, brand system (fonts/colors), splash, adding a case study, l10n reality | `ato-website-content-and-brand` | +| Writing widget/integration tests (must cover small-screen layouts), CI test step, deleting dead scaffold | `ato-website-testing-and-cleanup` | +| The cross-repo inquiries pipeline, Firestore rules posture + rules-export runbook, the dev_ prefix email gap | `ato-website-inquiries-and-integration` | + +This skill covers *how the code is wired*. Build/ship, look/copy, tests, and the +cross-repo/security posture are owned elsewhere. + +## Entry & bootstrap + +Three near-identical `lib/main_*.dart` entrypoints (`_development`, `_staging`, `_production`). +On web they differ only in log level: `main_development` = `Level.ALL`, +`main_production` = `Level.WARNING`. Each `main()` does, in order: +`WidgetsFlutterBinding.ensureInitialized()` → `Firebase.initializeApp(...)` → +`usePathUrlStrategy()` (behind `kIsWeb`; clean URLs, no `#`) → `bootstrap(() => App())`. + +`lib/bootstrap.dart` installs the global `Bloc.observer = AppBlocObserver()` (logs every +bloc `onChange`/`onError`), wires `FlutterError.onError`, and runs the app inside +`runZonedGuarded`. **Any new app-wide error/observer wiring goes in `bootstrap.dart`, not in +`main_*.dart`.** + +`lib/app/app.dart` is a one-line barrel: `export 'view/app.dart';`. The real `App` widget is +`lib/app/view/app.dart`. + +## Routing — one GoRouter, one ShellRoute + +`lib/app/view/app.dart` defines a single `GoRouter` (`_rootRouter`) with `initialLocation: '/'` +and exactly **one `ShellRoute`**. The shell's `builder` wraps every child page in +`HomeScreen(child: child)` — that is the persistent chrome (top menu + footer). Pages render +into `HomeScreen`'s `Expanded` slot; the footer (ATO wordmark + social icons) shows on every +route **except** `/` (guarded by `GoRouter.of(context).location != '/'`). + +Routes (source order in `app.dart:30-92`; order does not affect matching): + +| Path | Screen widget | File | +|---|---|---| +| `/` | `HomeScreenContent` | `lib/features/home/screen/home_screen_content.dart` | +| `/privacy` | `PrivacyScreen` | `lib/features/privacy/screen/privacy_screen.dart` | +| `/us` | `AboutUsScreen` | `lib/features/about_us/screen/about_us_screen.dart` | +| `/contact` | `ContactUsScreen` | `lib/features/contact_us/screen/contact_us_screen.dart` | +| `/projects` | `ProjectsScreen` | `lib/features/projects/screen/projects_screen.dart` | +| `/services` | `ServicesScreen` | `lib/features/services/screen/services_screen.dart` | + +- **Transitions:** every route builds its page via `buildPageWithDefaultTransition(...)` + from `lib/utils/router_utils.dart` — a `CustomTransitionPage` with a 100 ms fade in/out. + Reuse this helper for new routes; do not hand-roll a `CustomTransitionPage`. +- **Quirk:** the `/privacy` route declares **both** a `builder` and a `pageBuilder`. go_router + uses `pageBuilder` when present, so the `builder` is dead. New routes should declare + `pageBuilder` only. +- **Navigation is imperative:** widgets call `GoRouter.of(context).go('/path')` (see + `home_screen.dart`, `widget_utils.dart`, `contact_us_screen.dart`). There is no typed route + or `context.goNamed`. +- **Version pin (as of 2026-07-07):** `go_router` is locked to **6.5.2** + (`pubspec.yaml` pins `^6.5.2`). `GoRouter.of(context).location` used in + `home_screen.dart:58` is the **go_router 6 API**; it was removed in later majors (replaced by + `GoRouterState.of(context).uri`). A go_router bump must migrate that call. An abandoned + Dependabot branch proposes go_router 10 — treat the bump as a real migration, not a patch. + +Full route-add checklist + the ShellRoute wiring diagram: `references/routing-and-responsive.md`. + +## State management — bloc/cubit + +`flutter_bloc` **8.1.1** / `bloc` **8.1.0** (as of 2026-07-07). Inventory: + +| Class | Type | Provided at | Purpose | +|---|---|---|---| +| `ContactUsBloc` | `Bloc` (event/state) | `App.build` (`app.dart:110`), above the router → app-wide | Writes the contact form to Firestore (the only backend call) | +| `ServiceSelectCubit` | `Cubit` | `HomeScreen` via `MultiBlocProvider` (`home_screen.dart:27`) | Which of 3 home services is expanded (0/1/2) | +| `MenuSelectionCubit` | `Cubit` | `HomeScreen` via `MultiBlocProvider` (`home_screen.dart:30`) | Which nav item shows the "selected" indicator (0=none,1=about,…4=contact) | + +- The two cubits are `Cubit` with named mutators (`selectBranding()`, `selectAbout()`, …) + and boolean getters — trivial UI selection state, no models. +- **Scoping matters:** `ContactUsBloc` sits *above* the `GoRouter`, so it survives route changes + and is reachable from `/contact` via `BlocProvider.of(context)`. The two cubits + are provided *inside* `HomeScreen` (the shell), so they persist across route changes too but + are scoped to the shell subtree, and are what `home_screen_content.dart` reads. +- **Naming trap:** `ContactUsBloc.sendEmail(...)` sends **no email** — it validates non-empty + fields then adds a Firestore document. Email is sent by the separate functions repo reacting + to that write (see below). + +Full bloc/cubit event/state shapes and the contact write field-by-field: +`references/state-and-data-flow.md`. + +## Responsive — mutable static globals (load-bearing smell) + +`lib/utils/screen_utils.dart` classifies width into a `ScreenSize` enum via `getScreenSize(context)`: + +| Width (MediaQuery) | `ScreenSize` | +|---|---| +| `< 600` | `compact` (phone) | +| `< 840` | `medium` (tablet) | +| `>= 840` | `expanded` (desktop) | + +Instead of reading that at each use site, `App.build` (`app.dart:103-108`) writes the result into +**three mutable static booleans** on `lib/utils/constants.dart`: +`Constants.isExpandedScreen`, `Constants.isMediumScreen`, `Constants.isCompactScreen`. Widgets +then branch on those globals directly (e.g. `home_screen.dart:120 _buildMenu`, +`services_screen.dart:47`, `contact_us_screen.dart:73`, `widget_utils.dart:152`). + +**Why it is load-bearing, not just dead smell:** `App.build` calls `getScreenSize(context)`, +which reads `MediaQuery.of(context)`. That subscribes `App` to size changes, so a browser resize +rebuilds `App`, which **recomputes the globals before the rest of the tree rebuilds**. Break that +(e.g. stop reading MediaQuery in `App.build`) and the globals go stale on resize. + +**Rules of the road:** +- Do NOT read `Constants.is*Screen` inside code that runs *before* `App.build` (they default to + `isCompactScreen = true`, the others `false`). +- For **new** code, prefer reading layout at the point of use — `LayoutBuilder`, or + `getScreenSize(context: context)` directly (it is context-based and needs no globals) — rather + than adding more reads of the static globals. This keeps widgets testable in isolation. +- Tests **must** exercise compact/medium widths, not just desktop. Because layout hinges on these + globals + `MediaQuery`, a widget test must pump at a small size to hit the mobile branches. See + `ato-website-testing-and-cleanup`. + +## Shared widgets + +`lib/utils/widget_utils.dart`: +- `WidgetUtils.defaultBackground({context, child})` — black `ColoredBox` + `GradientUtils.singleGradient`, `SizedBox.expand`. The standard full-bleed screen background; `HomeScreen` wraps the whole app in it. +- `WidgetUtils.extendedBackground({context, height, width?, child})` — scrollable, two-pass `GradientUtils.extendedGradient`. +- `WidgetUtils.showDetailNarrative({context, detailText})` — a `Text.rich` narrative with a "Talk to us today!" link that `go`s to `/contact`. +- `HoveredText` — the stateful nav/label widget (underline-on-hover or filled indicator, Plavsky font by default). Drives desktop menu items and `MasterDetailScreen` titles; supports `navigateTo` (calls `GoRouter.go`) and/or `onTap`. + +`GradientUtils` (`lib/utils/gradient_utils.dart`) holds the two `LinearGradient`s used above +(colors from `AppColors`; the palette is owned by `ato-website-content-and-brand`). + +**`MasterDetailScreen` — correction to fix in your mental model:** it is **NOT** in +`widget_utils.dart`. It is defined in +`lib/features/projects/screen/projects_content_screen.dart:11` +(the file misleadingly named for projects). It is the shared master/detail layout used by **both** +`ProjectsScreen` (`projects_screen.dart:19`) and `ServicesScreen` +(`services_screen.dart:48,69`): a title list where tapping an item reveals a narrative + optional +detail widget. Reuse it for any future "list of things, tap to expand" page rather than rebuilding it. + +## Firebase init — web only + +`lib/firebase_options.dart` is FlutterFire-generated but configured for **web only**: +`DefaultFirebaseOptions.currentPlatform` returns `web` under `kIsWeb` and **throws +`UnsupportedError` for android/iOS/macOS/windows/linux**. The `android/`, `ios/`, `windows/` +scaffolds are vestigial (web-only is the product decision — see `ato-website-content-and-brand` +and the cleanup backlog in `ato-website-testing-and-cleanup`). Do not assume you can `flutter run` +this on a device; Firebase init will throw. + +`lib/utils/platform_stub.dart` provides a no-op `usePathUrlStrategy()` for non-web via the +conditional import in `main_*.dart` (`if (dart.library.html)` swaps in the real +`flutter_web_plugins` one). On web, the real clean-URL strategy is used. + +## Contact-form data flow (website side) + +`ContactUsScreen` (`contact_us_screen.dart`) collects Full name / Email / Message via three +`TextEditingController`s and calls `contactUsBloc.sendEmail(...)`. That dispatches `SendEmailEvent`; +`ContactUsBloc._handleSendEmailEvent` (`contact_us_bloc.dart:36`) writes **one Firestore document**: + +```dart +FirebaseFirestore.instance + .collection('${_firestore_collection_prefix}inquiries') + .add({ + 'email_address': event.email, + 'full_name': event.fullName, + 'message': event.message, + 'inquired_on': DateTime.now().millisecondsSinceEpoch, +}); +``` + +State progression on the screen: `ContactUsErrorState` → SnackBar; `ContactUsSuccessState` → +"We received your message!" dialog (see the `BlocListener` in `contact_us_screen.dart:22`). + +The written collection is `${_firestore_collection_prefix}inquiries` (`contact_us_bloc.dart:26`). +**Where this skill stops:** the prefix/`ENVIRONMENT` reality (why prod writes plain `inquiries`), +the by-design `dev_inquiries` email gap, the separate functions-repo trigger that turns the write +into email, and the Firestore rules posture are all owned by +`ato-website-inquiries-and-integration` — go there, not here. This skill covers only the website +side up to the `.add()` write. + +## Adding a new page (quick runbook) + +1. Create `lib/features//screen/_screen.dart` (a `StatelessWidget`/`StatefulWidget`); + wrap content in a `SingleChildScrollView` — it renders inside `HomeScreen`'s `Expanded` slot, + which already supplies the gradient background and the menu/footer chrome. +2. Add a `GoRoute` inside the single `ShellRoute` in `lib/app/view/app.dart`, using + `buildPageWithDefaultTransition` in its `pageBuilder`. +3. For a nav entry: add a case in `HomeScreen._buildMenu` (both the compact `PopupMenuButton` + branch and the expanded `HoveredText` row) and a `MenuSelectionCubit.select…()` mutator. +4. Branch responsive layout via `getScreenSize(context)` / `Constants.is*Screen`; verify at + `<600` width. +5. Reuse `MasterDetailScreen` for tap-to-expand list pages, and `WidgetUtils.showDetailNarrative` + for narrative bodies with a contact CTA. + +## Provenance and maintenance + +Authored 2026-07-07 from direct inspection of the repo (git history is shallow; no invented +history). Volatile facts are date-stamped inline. Re-verify with: + +```bash +cd /Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website +# routes, shell, transition helper, responsive globals, prefix logic, Firebase web-only, +# MasterDetailScreen true home, and pinned versions — all in one pass: +.claude/skills/ato-website-architecture/scripts/verify-architecture.sh +``` + +If that script reports mismatches, fix the affected table/section here; the script only checks +that the anchors still exist, not their surrounding prose. diff --git a/.claude/skills/ato-website-architecture/references/routing-and-responsive.md b/.claude/skills/ato-website-architecture/references/routing-and-responsive.md new file mode 100644 index 0000000..0081e4b --- /dev/null +++ b/.claude/skills/ato-website-architecture/references/routing-and-responsive.md @@ -0,0 +1,101 @@ +# Routing + responsive — deep reference + +Companion to `../SKILL.md`. Paths relative to repo root +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website`. +Verified 2026-07-07. + +## The ShellRoute wiring, concretely + +`lib/app/view/app.dart` builds ONE router. Structure: + +``` +GoRouter(navigatorKey: rootNavigatorKey, initialLocation: '/') +└─ ShellRoute + ├─ builder: (context, state, child) => HomeScreen(child: child) // persistent chrome + └─ routes: [ GoRoute('/'), GoRoute('/privacy'), GoRoute('/us'), + GoRoute('/contact'), GoRoute('/projects'), GoRoute('/services') ] +``` + +- `HomeScreen` (`lib/features/home/screen/home_screen.dart`) is the chrome: it paints the + full-bleed `WidgetUtils.defaultBackground`, a centered `SizedBox(width: 1000)` column with the + top menu (`_buildMenu`), the routed `child` in an `Expanded`, and a footer. +- The footer (ATO wordmark + Facebook/LinkedIn SVGs) renders only when + `GoRouter.of(context).location != '/'` — i.e. hidden on the landing page. +- `_buildMenu` has two layouts, chosen by `Constants.isExpandedScreen`: + desktop → a `Row` of `HoveredText` items; compact/medium → a `PopupMenuButton` hamburger. + Both mutate `MenuSelectionCubit` and call `GoRouter.of(context).go(...)`. +- `App.rootNavigatorKey` is a `static final GlobalKey` passed to the router. + The `ShellRoute` itself has no separate navigator key (single-navigator shell). + +## Page transition helper + +`lib/utils/router_utils.dart` — every route must use this rather than a raw page: + +```dart +buildPageWithDefaultTransition( + context: context, state: state, child: (), +) +``` + +It returns a `CustomTransitionPage` keyed by `state.pageKey`, 100 ms fade in and out +(`FadeTransition(opacity: animation)`). + +## go_router version reality (as of 2026-07-07) + +- `pubspec.yaml` pins `go_router: ^6.5.2`; `pubspec.lock` resolves **6.5.2**. +- `home_screen.dart:58` uses `GoRouter.of(context).location`. This getter is **go_router 6 API** + and was removed in later majors. If you bump go_router: + - replace `.location` with `GoRouterState.of(context).uri.toString()` (or `.path`), + - re-check `ShellRoute`/`GoRoute` signatures (they changed across 7→10), + - test every `.go('/...')` call. +- An abandoned Dependabot branch proposes go_router 10.0.0. Treat any bump as a migration task, + coordinated with the owner; it is not a drop-in patch. + +## Responsive globals — full rationale and the safe migration path + +Source of truth for breakpoints: `lib/utils/screen_utils.dart`. + +```dart +ScreenSize getScreenSize({required BuildContext context}) { + final w = MediaQuery.of(context).size.width; + if (w < 600) return ScreenSize.compact; // phone + if (w < 840) return ScreenSize.medium; // tablet + return ScreenSize.expanded; // desktop +} +``` + +`lib/utils/constants.dart` holds the mutable mirror: + +```dart +class Constants { + static bool isExpandedScreen = false; + static bool isMediumScreen = false; + static bool isCompactScreen = true; // default before App.build runs +} +``` + +`App.build` (`app.dart:103-108`) recomputes all three from `getScreenSize(context)` on every +build. Because `getScreenSize` reads `MediaQuery.of(context)`, `App` is subscribed to size +changes, so a resize rebuilds `App` first and refreshes the globals before descendants rebuild. +That ordering is the entire reason the pattern is correct rather than merely lucky. + +**Known hazards:** +- The globals are process-global mutable state. In a `flutter test`, they carry the value from + whatever last ran unless you pump `App` (or set them) at your target size. Prefer pumping the + real widget under `MediaQuery` with a chosen `Size` so `App.build` sets them for you. +- Reading them from code that executes before the first `App.build` yields the defaults + (`isCompactScreen = true`). +- `constants.dart` imports `package:intl` but uses nothing from it (a stale unused import; the + analyze gate was relaxed to tolerate such debt — see `ato-website-build-deploy`). + +**Recommended for new code:** read layout locally instead of adding new global reads — +`LayoutBuilder`, `MediaQuery.sizeOf(context)`, or `getScreenSize(context: context)` at the use +site. This makes a widget self-contained and testable without booting `App`. Don't do a +big-bang rewrite of existing global reads unless the owner asks; just stop growing them. + +## Consumers of `Constants.is*Screen` (as of 2026-07-07) + +`grep -rn "Constants.is" lib/` — expect hits in `widget_utils.dart` (HoveredText corner radii), +`home_screen.dart` (`_buildMenu` desktop vs hamburger), `services_screen.dart` (two-column vs +stacked `MasterDetailScreen`), and `contact_us_screen.dart` (form width: full vs 350). Any new +responsive branch should be added deliberately and covered by a small-screen test. diff --git a/.claude/skills/ato-website-architecture/references/state-and-data-flow.md b/.claude/skills/ato-website-architecture/references/state-and-data-flow.md new file mode 100644 index 0000000..628a1b0 --- /dev/null +++ b/.claude/skills/ato-website-architecture/references/state-and-data-flow.md @@ -0,0 +1,95 @@ +# State + contact-form data flow — deep reference + +Companion to `../SKILL.md`. Paths relative to repo root +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website`. +Verified 2026-07-07. `flutter_bloc` 8.1.1 / `bloc` 8.1.0, `cloud_firestore` 4.6.0, +`firebase_core` 2.11.0. + +## Global bloc observer + +`lib/bootstrap.dart` sets `Bloc.observer = AppBlocObserver()`. `AppBlocObserver` overrides +`onChange` and `onError` to `log(...)` (dart:developer). Every cubit/bloc transition and error is +logged app-wide. This is the one place bloc lifecycle is observed; add cross-cutting bloc logging +here, not per-bloc. + +## The two selection cubits + +Both are `Cubit` holding a single index — no models, no async. + +`lib/features/home/cubit/service_select_cubit.dart`: +```dart +class ServiceSelectCubit extends Cubit { + ServiceSelectCubit() : super(0); + void selectBranding() => emit(0); + void selectPdd() => emit(1); + void selectVt() => emit(2); +} +``` +Read by `home_screen_content.dart` (`BlocBuilder`) to pick which service +blurb is shown on `/`. + +`lib/features/home/cubit/menu_selection_cubit.dart` — index 0..4 (0 = none, 1 = about, +2 = services, 3 = projects, 4 = contact) with `select…()` mutators and `is…Selected` getters. +Read by `HomeScreen._buildMenu` to highlight the active nav item. + +Both are provided together in `HomeScreen` via `MultiBlocProvider` (`home_screen.dart:25`), so +they live for the lifetime of the shell and reset only on a full app restart. + +## ContactUsBloc — the only backend call + +Files: `lib/features/contact_us/bloc/contact_us_bloc.dart` (+ `contact_us_event.dart`, +`contact_us_state.dart` as `part` files). + +Events: +- `SendEmailEvent({email, fullName, message})` — the only concrete event. + +States (all extend `abstract ContactUsState`): +- `ContactUsInitial` +- `ContactUsLoadingState({isLoading = false, message})` +- `ContactUsSuccessState({message, data})` +- `ContactUsErrorState({required message, data})` + +Public API used by the screen: +- `sendEmail({email, fullName, message})` — synchronous guard: if any field is empty it emits + `ContactUsErrorState('Please fill up all fields')` and returns; otherwise it `add`s a + `SendEmailEvent`. **Misnamed — it does not send email.** +- `isSendingEmail` getter — a plain `bool` field (`_isSendingEmail`) flipped inside the handler; + the screen uses it to disable the form/button while a write is in flight. Note it is *not* part + of the emitted state, so the UI relies on `BlocBuilder` rebuilds triggered by the loading-state + emissions to re-read it. + +Handler `_handleSendEmailEvent` (`contact_us_bloc.dart:36`): +1. set `_isSendingEmail = true`; `emit(ContactUsLoadingState(isLoading: true))` +2. `await FirebaseFirestore.instance.collection('${prefix}inquiries').add({...})` +3. on success: `_isSendingEmail = false`; `emit(ContactUsLoadingState())`; `emit(ContactUsSuccessState())` +4. on error: `emit(ContactUsLoadingState())`; `log.severe(...)`; `emit(ContactUsErrorState('Something went wrong while sending emails'))` + +Document fields written to `inquiries` (exact keys — the functions repo consumes these): + +| Firestore field | Source | Type | +|---|---|---| +| `email_address` | `event.email` | String | +| `full_name` | `event.fullName` | String | +| `message` | `event.message` | String | +| `inquired_on` | `DateTime.now().millisecondsSinceEpoch` | int (epoch ms) | + +If you change a field name here, it is a coordinated cross-repo schema change — the functions +repo trigger reads these keys. Do not rename one side alone. (Pipeline + contract ownership: +`ato-website-inquiries-and-integration`.) + +## Collection prefix logic + +`_firestore_collection_prefix` (`contact_us_bloc.dart:26`): +```dart +const env = String.fromEnvironment('ENVIRONMENT'); +if (env == 'development') return 'dev_'; +return ''; +``` +`String.fromEnvironment` is compile-time (`--dart-define=ENVIRONMENT=...`). Result: +`development` → `dev_inquiries`; staging/production/empty → `inquiries`. The CI/deployed build +sets no `ENVIRONMENT` → writes `inquiries`. A dev build writing `dev_inquiries` triggers no email +because no function watches that collection (by-design; use emulators). Details: +`ato-website-inquiries-and-integration`. + +Style nit: `_firestore_collection_prefix` is `snake_case` (Dart convention is `lowerCamelCase`); +tolerated because the analyze gate was relaxed (see `ato-website-build-deploy`). diff --git a/.claude/skills/ato-website-architecture/scripts/verify-architecture.sh b/.claude/skills/ato-website-architecture/scripts/verify-architecture.sh new file mode 100755 index 0000000..42fcd0a --- /dev/null +++ b/.claude/skills/ato-website-architecture/scripts/verify-architecture.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Re-verify the load-bearing facts in ato-website-architecture/SKILL.md against the repo. +# Read-only. Prints PASS/FAIL per anchor; exits non-zero if any FAIL. +# Authored 2026-07-07. Run from anywhere. +set -u + +REPO="/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website" +cd "$REPO" || { echo "repo not found: $REPO"; exit 2; } + +fail=0 +check() { # desc, command... + local desc="$1"; shift + if "$@" >/dev/null 2>&1; then + printf 'PASS %s\n' "$desc" + else + printf 'FAIL %s\n' "$desc"; fail=1 + fi +} + +check "single ShellRoute in app.dart" grep -q "ShellRoute(" lib/app/view/app.dart +check "shell wraps HomeScreen(child: child)" grep -q "HomeScreen(child: child)" lib/app/view/app.dart +check "transition helper exists" grep -q "buildPageWithDefaultTransition" lib/utils/router_utils.dart +check "go_router 6 .location still used" grep -q "GoRouter.of(context).location" lib/features/home/screen/home_screen.dart +check "breakpoints 600/840 intact" grep -q "deviceWidth < 600" lib/utils/screen_utils.dart +check "responsive globals recomputed in App" grep -q "Constants.isExpandedScreen =" lib/app/view/app.dart +check "Constants static booleans present" grep -q "static bool isCompactScreen" lib/utils/constants.dart +check "prefix logic dev_ on development" grep -q "return 'dev_';" lib/features/contact_us/bloc/contact_us_bloc.dart +check "writes {prefix}inquiries collection" grep -q "inquiries" lib/features/contact_us/bloc/contact_us_bloc.dart +check "MasterDetailScreen home = projects_content_screen.dart" \ + grep -q "class MasterDetailScreen" lib/features/projects/screen/projects_content_screen.dart +check "Firebase web-only (android throws)" grep -q "have not been configured for android" lib/firebase_options.dart + +echo "--- pinned versions (as of 2026-07-07: go_router 6.5.2, flutter_bloc 8.1.1) ---" +for p in go_router flutter_bloc bloc cloud_firestore firebase_core; do + v=$(awk "/^ $p:/{f=1} f&&/version:/{print \$2; exit}" pubspec.lock) + printf ' %-16s %s\n' "$p" "$v" +done + +exit $fail diff --git a/.claude/skills/ato-website-build-deploy/SKILL.md b/.claude/skills/ato-website-build-deploy/SKILL.md new file mode 100644 index 0000000..1c5fbdc --- /dev/null +++ b/.claude/skills/ato-website-build-deploy/SKILL.md @@ -0,0 +1,210 @@ +--- +name: ato-website-build-deploy +description: >- + Use when building, running, or deploying the Anaheim Technologies marketing + website (Flutter web) — reproducing the CI build locally, setting up the + fvm-pinned toolchain, or shipping a change. Use when `flutter` is "command not + found", when a push to master did (or did not) reach the live site, when a PR + preview channel is expected, when a Firebase Hosting deploy to + `anaheim-technologies` fails, or when you need to know what `main_development` + vs `main_production` vs the `ENVIRONMENT` dart-define actually change. +--- + +# ato-website build & deploy + +The runbook for compiling and shipping the Anaheim Technologies marketing site: +a Flutter **web-only** app (Firebase Hosting, project `anaheim-technologies`, +live at `https://anaheimtechnologies.com`). This is the PRIMARY HOME for the +toolchain, build/run/deploy commands, the CI/CD pipeline, the prod-only +single-project reality, and the known build-time traps. + +## When NOT to use this skill (use a sibling in THIS repo) + +- **lib/ structure, routing, blocs, responsive globals, Firebase init in Dart** → + `ato-website-architecture`. +- **Content, pages, brand/fonts/colors, splash screen, l10n copy** → + `ato-website-content-and-brand`. +- **Tests, the broken counter scaffold, removing mobile/Windows/es debt, adding a + CI test step** → `ato-website-testing-and-cleanup`. +- **The contact-form → Firestore `inquiries` → email pipeline, the `dev_inquiries` + gap, Firestore rules** → `ato-website-inquiries-and-integration`. + +## Toolchain (as of 2026-07-07) + +| Thing | Value | How to confirm | +|---|---|---| +| Flutter SDK manager | **fvm** (`/opt/homebrew/bin/fvm`, v4.0.4) | `fvm --version` | +| Pinned Flutter | **3.41.6** (Dart 3.11.4) via `.fvmrc` | `cat .fvmrc` → `{"flutter":"3.41.6"}` | +| SDK location | `.fvm/flutter_sdk` → `~/fvm/versions/3.41.6` | `readlink .fvm/flutter_sdk` | +| Lint ruleset | `very_good_analysis` 3.1.0 | `cat analysis_options.yaml` | +| Firebase CLI (deploy) | `firebase` v14.27.0 | `firebase --version` | +| Node (`.nvmrc`) | `v19.1.0` — only for `firebase-tools`; NO JS tooling lives in this repo | `cat .nvmrc` | + +**There is NO system `flutter` or `dart` on PATH** (`which flutter` → not found). +**ALWAYS prefix `fvm`** — run `fvm flutter …` / `fvm dart …`, never bare +`flutter`. A bare `flutter` command will fail with "command not found"; if it +ever resolves, it is the wrong (unpinned) SDK. First thing to verify in a fresh +clone: + +```bash +fvm flutter --version # must print "Flutter 3.41.6 ... Dart 3.11.4" +``` + +(In CI the SDK is provisioned differently — `subosito/flutter-action` puts a +bare `flutter` 3.41.6 on PATH — so the CI YAML calls bare `flutter`. That is +correct **for CI only**; locally, always use `fvm flutter`.) + +## Core commands (verified) + +Run all from the repo root. + +```bash +# 1. Resolve dependencies (pinned by pubspec.lock) +fvm flutter pub get + +# 2. Regenerate localizations (l10n.yaml; outputs into lib/l10n/arb/, which is +# tracked/committed — regenerate after editing any .arb). +fvm flutter gen-l10n + +# 3. Run the dev flavor in Chrome (this is the everyday local command) +fvm flutter run -d chrome -t lib/main_development.dart \ + --dart-define=ENVIRONMENT=development + +# 4. Build exactly what CI ships → output lands in build/web (gitignored) +fvm flutter build web --release -t lib/main_production.dart + +# 5. Manual deploy of a local build to Firebase Hosting (needs `firebase login` +# + build/web present). Normally you do NOT do this by hand — see "How +# changes ship". Uses the default project from .firebaserc (anaheim-technologies). +firebase deploy --only hosting +``` + +`scripts/build-web.sh` chains steps 1–4 (the CI build) for you. + +## The three `main_*.dart` entrypoints (they barely differ on web) + +All three call `bootstrap()` (global `AppBlocObserver`, zoned error logging) and +render the same `App`. On web the practical differences are tiny: + +| Entrypoint | Log level | `Firebase.initializeApp`? | Notes | +|---|---|---|---| +| `main_development.dart` | `Level.ALL` | yes | everyday local target | +| `main_production.dart` | `Level.WARNING` | yes | **what CI builds & deploys** | +| `main_staging.dart` | (none) | **NO** | minimal stub — does not init Firebase, so the contact form's Firestore write cannot succeed under this target. Not used by CI or the VSCode/IntelliJ prod path. Prefer dev or production. | + +`--flavor` (development/staging/production) exists in the run configs but is an +Android/iOS concept; the web build (CI and step 4 above) passes **no** `--flavor` +and it is irrelevant to the deployed artifact. Full detail: +`references/entrypoints-and-environment.md`. + +## The `ENVIRONMENT` dart-define inconsistency (intended, benign) + +`String.fromEnvironment('ENVIRONMENT')` is read in two places; the only one that +changes behavior is the contact form's Firestore collection prefix +(`lib/features/contact_us/bloc/contact_us_bloc.dart`: `development` → `dev_` +prefix, anything else → unprefixed `inquiries`). + +**Who passes the define:** + +| Launch path | Passes `--dart-define=ENVIRONMENT`? | +|---|---| +| IntelliJ `.idea/runConfigurations/development.xml` | `=development` | +| IntelliJ `production.xml` | `=production` (also `--web-renderer html`) | +| IntelliJ `staging.xml` | none | +| `.vscode/launch.json` (all 3) | **none** | +| CI (`.github/workflows/main.yaml`) | **none** | + +So **the deployed build has an empty `ENVIRONMENT`.** Empty `!= 'development'`, +so the prod build writes to the unprefixed `inquiries` collection — which is +exactly the collection the Cloud Functions trigger watches. This is the intended +prod path, not a bug. The full explanation of why `dev_inquiries` never triggers +email lives in `ato-website-inquiries-and-integration` (do not duplicate it here). + +## How changes ship (this repo has no separate change-control doc — this is it) + +There is one workflow: `.github/workflows/main.yaml` (`ci-deploy`). It is the +only deploy path you should use. + +- **Push to `master` → deploys LIVE.** A merge/push to `master` auto-builds and + deploys to the `live` Hosting channel — i.e. straight to production + `anaheimtechnologies.com`. There is no manual approval gate. +- **Open a PR against `master` (same repo) → preview channel.** The same build + deploys to an ephemeral preview channel and the action comments the preview + URL on the PR. **Use PRs to eyeball a change before it goes live.** PRs from + forks are skipped (no secret access). +- **`workflow_dispatch`** (manual "Run workflow") → also deploys LIVE. + +CI steps, in order: `checkout` → `subosito/flutter-action@v2` (3.41.6, stable, +cached) → `flutter pub get` → `flutter gen-l10n` → `flutter analyze +--no-fatal-warnings --no-fatal-infos` → `flutter build web --release -t +lib/main_production.dart` → deploy via `FirebaseExtended/action-hosting-deploy@v0` +to project `anaheim-technologies`. + +**There is NO `flutter test` step** (the tests are broken scaffold; a test gate +is wanted once real tests exist — see `ato-website-testing-and-cleanup`). Secrets +used: `GITHUB_TOKEN`, `FIREBASE_SERVICE_ACCOUNT_ANAHEIM_TECHNOLOGIES`. Full +step-by-step, trigger conditions, and secret-provisioning notes: +`references/ci-cd.md`. + +### The analyze gate is deliberately errors-only + +Commit `2816a39` (website repo, "ci: relax analyze gate to errors only") changed +the CI analyze step from `--no-fatal-infos` to +`--no-fatal-warnings --no-fatal-infos` so that **pre-existing unused-import +warnings do not block deploy.** Only true compile errors fail the gate. The +offending imports are still present (verified 2026-07-07, all with zero usages): + +- `lib/features/services/screen/services_screen.dart` — `import 'dart:math' as math;`, `import 'package:url_launcher/url_launcher_string.dart';` +- `lib/features/projects/screen/projects_screen.dart` — same two unused imports +- `lib/utils/constants.dart` — `import 'package:intl/intl.dart';` + +Fixing this lint debt (so the gate can be tightened back) is the cleanup skill's +job, not this one → `ato-website-testing-and-cleanup`. + +## Environment reality (accepted) + +**One Firebase project, `anaheim-technologies`, prod-only.** `.firebaserc` has a +single `default` project; `firebase.json` is hosting-only (`public: build/web`, +SPA rewrite `** → /index.html`; no `firestore.rules`, no `functions/` in this +repo). There is no separate dev/staging Firebase project. This is the accepted +reality — emulators are the local dev path. (The Firestore/inquiries and rules +posture belong to `ato-website-inquiries-and-integration`.) + +## Traps + +- **`firebase_options.dart` is web-only.** `currentPlatform` returns config only + when `kIsWeb`; every other platform (`android`/`iOS`/`macOS`/`windows`/`linux`) + `throw UnsupportedError`. Do not attempt a mobile/desktop build — it will throw + at Firebase init. The `android/`, `ios/`, `windows/` scaffolds are vestigial + (removable debt per the maintainer — see cleanup skill). +- **No system `flutter`/`dart`** — always `fvm flutter` (see Toolchain). +- **`build/` is gitignored** (0 tracked files); the site is rebuilt in CI, never + committed. Do not commit `build/web`. +- **`main_staging.dart` does not init Firebase** — the contact form breaks under + it. Use `main_development.dart` (or `main_production.dart`) locally. +- **`firebase deploy --only hosting` by hand ships to LIVE** using the default + project. Prefer the PR-preview flow; only deploy by hand with intent. + +## Provenance and maintenance + +Authored **2026-07-07** from direct inspection of the repo at +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website` +(git `master`, HEAD `95e1d9d`). Re-verify drift-prone facts: + +```bash +cat .fvmrc # pinned Flutter version +fvm flutter --version # actual SDK (expect 3.41.6 / Dart 3.11.4) +cat .github/workflows/main.yaml # CI triggers, steps, analyze flags, channels, secrets +cat .firebaserc firebase.json # single project + hosting-only config +git log --oneline -1 2816a39 # the analyze-gate relax commit +# unused imports that keep the analyze gate at errors-only: +grep -nE "dart:math|url_launcher_string" lib/features/services/screen/services_screen.dart lib/features/projects/screen/projects_screen.dart +grep -n "package:intl/intl.dart" lib/utils/constants.dart +``` + +**Open/unverified:** whether the Firebase service-account secret and Hosting +target are still valid is not checkable from the repo (verify in the GitHub repo +settings + Firebase console). A full local `fvm flutter build web --release` +compiles the toolchain end-to-end but was not run to completion during authoring +(`fvm flutter pub get` was confirmed to resolve cleanly); run `scripts/build-web.sh` +to confirm the whole chain. diff --git a/.claude/skills/ato-website-build-deploy/references/ci-cd.md b/.claude/skills/ato-website-build-deploy/references/ci-cd.md new file mode 100644 index 0000000..86f86a2 --- /dev/null +++ b/.claude/skills/ato-website-build-deploy/references/ci-cd.md @@ -0,0 +1,103 @@ +# CI/CD reference — `.github/workflows/main.yaml` + +Verified against the repo 2026-07-07 (HEAD `95e1d9d`). This is the single source +of automated build + deploy for the website. There is exactly one workflow file +and one job. + +## Identity & triggers + +```yaml +name: ci-deploy +on: + push: { branches: [master] } + pull_request: { branches: [master] } + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +``` + +- Runs on any **push to `master`**, any **PR targeting `master`**, and manual + **Run workflow** (`workflow_dispatch`). +- `concurrency` cancels an in-progress run for the same ref when a newer one + starts (so rapid pushes to `master` don't stack deploys). + +## The job: `build-and-deploy` (runs-on `ubuntu-latest`) + +| # | Step | Command / action | +|---|---|---| +| 1 | Checkout | `actions/checkout@v4` | +| 2 | Set up Flutter | `subosito/flutter-action@v2` with `flutter-version: '3.41.6'`, `channel: 'stable'`, `cache: true` | +| 3 | Install dependencies | `flutter pub get` | +| 4 | Generate localizations | `flutter gen-l10n` | +| 5 | Analyze (errors only) | `flutter analyze --no-fatal-warnings --no-fatal-infos` | +| 6 | Build web (production) | `flutter build web --release -t lib/main_production.dart` | +| 7 | Deploy LIVE | `FirebaseExtended/action-hosting-deploy@v0`, **only if** `github.event_name == 'push' \|\| github.event_name == 'workflow_dispatch'` | +| 8 | Deploy PREVIEW | `FirebaseExtended/action-hosting-deploy@v0`, **only if** `github.event_name == 'pull_request' && head repo == this repo` | + +Notes: + +- CI calls **bare `flutter`** (not `fvm flutter`) — `subosito/flutter-action` + installs SDK 3.41.6 and puts it on PATH. This is correct in CI only; locally + use `fvm flutter` (there is no system Flutter on the dev machine). +- CI passes **no `--flavor`** and **no `--dart-define=ENVIRONMENT`**. That is why + the deployed build has an empty `ENVIRONMENT` and writes to the unprefixed + `inquiries` collection (see `references/entrypoints-and-environment.md`). +- **No `flutter test` step exists.** Adding one is tracked by + `ato-website-testing-and-cleanup`. + +## Deploy action inputs + +Both deploy steps use the same action and secrets; they differ only in whether a +`channelId` is set. + +```yaml +# Step 7 — LIVE (push / workflow_dispatch) +with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ANAHEIM_TECHNOLOGIES }} + projectId: anaheim-technologies + channelId: live # <-- deploys to production + +# Step 8 — PREVIEW (same-repo PR) +with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_ANAHEIM_TECHNOLOGIES }} + projectId: anaheim-technologies + # no channelId -> action creates/updates an ephemeral preview channel and + # comments the preview URL on the PR +``` + +- `channelId: live` = production Hosting channel. +- Omitting `channelId` on the PR step = a temporary preview channel; the action + posts the preview URL as a PR comment. Preview channels expire on their own. +- **Fork PRs are skipped** by the `head.repo.full_name == github.repository` + guard (forks cannot read the service-account secret). + +## Secrets (GitHub repo → Settings → Secrets and variables → Actions) + +| Secret | Purpose | +|---|---| +| `GITHUB_TOKEN` | auto-provided by Actions; lets the deploy action comment the preview URL on PRs | +| `FIREBASE_SERVICE_ACCOUNT_ANAHEIM_TECHNOLOGIES` | JSON key of a service account with Firebase Hosting deploy rights on project `anaheim-technologies` | + +If `FIREBASE_SERVICE_ACCOUNT_ANAHEIM_TECHNOLOGIES` is missing/expired, the deploy +steps fail; the build steps (2–6) still run and gate PRs. Whether the secret is +currently valid is **not checkable from the repo** — verify in GitHub settings + +the Firebase console. + +## History: the analyze-gate relax + +`git show 2816a39` (website repo, "ci: relax analyze gate to errors only", +2026-04-30, David + Claude Opus 4.7): changed step 5 from +`--no-fatal-infos` to `--no-fatal-warnings --no-fatal-infos` so pre-existing +unused-import **warnings** stop blocking deploy. Only true compile errors fail +the gate now. The unused imports are named in SKILL.md; cleaning them so the gate +can be re-tightened is `ato-website-testing-and-cleanup`'s job. + +## Re-verify + +```bash +cat .github/workflows/main.yaml +git show 2816a39 -- .github/workflows/main.yaml +``` diff --git a/.claude/skills/ato-website-build-deploy/references/entrypoints-and-environment.md b/.claude/skills/ato-website-build-deploy/references/entrypoints-and-environment.md new file mode 100644 index 0000000..2bc9f4c --- /dev/null +++ b/.claude/skills/ato-website-build-deploy/references/entrypoints-and-environment.md @@ -0,0 +1,103 @@ +# Entrypoints & the `ENVIRONMENT` dart-define + +Verified against the repo 2026-07-07. This is the detail behind the "three +`main_*.dart` entrypoints" and "`ENVIRONMENT` inconsistency" summaries in +SKILL.md. + +## The three entrypoints + +All three live in `lib/` and end by calling `bootstrap(() => App())` from +`lib/bootstrap.dart` (which installs a global `AppBlocObserver` and a +`runZonedGuarded` error handler). On **web** the behavioral differences are +minimal: + +### `lib/main_development.dart` +```dart +Logger.root.level = Level.ALL; // verbose logs +// ... onRecord listener prints every record ... +WidgetsFlutterBinding.ensureInitialized(); +await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); +if (kIsWeb) { usePathUrlStrategy(); } +const env = String.fromEnvironment('ENVIRONMENT'); // read + logged +bootstrap(() => App()); +``` + +### `lib/main_production.dart` +Identical to development **except** `Logger.root.level = Level.WARNING`. This is +the entrypoint CI builds and deploys (`flutter build web --release -t +lib/main_production.dart`). + +### `lib/main_staging.dart` +Materially different — a minimal stub: +```dart +void main() { + if (kIsWeb) { usePathUrlStrategy(); } + bootstrap(() => App()); +} +``` +- **No `Firebase.initializeApp`.** Any code path that touches Firestore (e.g. the + contact form `.add()` in `contact_us_bloc.dart`) will fail at runtime because + Firebase was never initialized. +- No logging configuration, no `ENVIRONMENT` read. +- Not used by CI, and not the IntelliJ/VSCode "production" path. **Avoid it** for + anything that exercises the backend; use development or production. + +### Net +`main_development` vs `main_production` differ only in log verbosity on web. +`main_staging` is a near-empty stub missing Firebase init. The `--flavor` +(development/staging/production) in the run configs is an Android/iOS build-flavor +concept and does not affect the web artifact; CI passes no `--flavor`. + +## `ENVIRONMENT` — where it's read and what it changes + +`String.fromEnvironment('ENVIRONMENT')` (a compile-time constant, injected via +`--dart-define`) appears in: + +1. `main_development.dart` / `main_production.dart` — read into `env` and logged. + Cosmetic only. +2. `lib/features/contact_us/bloc/contact_us_bloc.dart` — + `_firestore_collection_prefix` returns `'dev_'` when `env == 'development'`, + else `''`. This is the **only** place `ENVIRONMENT` changes behavior: + +```dart +String get _firestore_collection_prefix { + const env = String.fromEnvironment('ENVIRONMENT'); + if (env == 'development') { return 'dev_'; } + return ''; +} +// ... writes to collection('${_firestore_collection_prefix}inquiries') +``` + +So: +- `ENVIRONMENT=development` → writes to `dev_inquiries`. +- any other value **or empty** → writes to `inquiries`. + +## Who passes the define (the inconsistency) + +| Launch path | File | `--dart-define=ENVIRONMENT` | +|---|---|---| +| IntelliJ development | `.idea/runConfigurations/development.xml` | `=development` | +| IntelliJ production | `.idea/runConfigurations/production.xml` | `=production` (+ `--web-renderer html`) | +| IntelliJ staging | `.idea/runConfigurations/staging.xml` | none (+ `--web-renderer html`) | +| VSCode (all 3) | `.vscode/launch.json` | **none** (only `--flavor` + `--target`) | +| CI build | `.github/workflows/main.yaml` | **none** | + +## Consequence for the deployed site (intended, benign) + +The CI production build passes no `--dart-define`, so `ENVIRONMENT` is empty in +the shipped bundle. Empty `!= 'development'` → the prefix is `''` → the contact +form writes to `inquiries`, which is the collection the Cloud Functions +`inquiryCreated` trigger watches. That is the intended production wiring. + +The corollary — a **development** build (with `ENVIRONMENT=development`) writes to +`dev_inquiries`, a collection no deployed trigger watches, so no email is sent — +is a by-design gap. Full treatment of that pipeline and gap is in the sibling +skill `ato-website-inquiries-and-integration` (not duplicated here). + +## Re-verify + +```bash +grep -n "String.fromEnvironment('ENVIRONMENT')" lib/main_development.dart lib/main_production.dart lib/features/contact_us/bloc/contact_us_bloc.dart +grep -rn "dart-define=ENVIRONMENT" .idea/runConfigurations .vscode .github +grep -n "Level.ALL\|Level.WARNING\|initializeApp" lib/main_development.dart lib/main_production.dart lib/main_staging.dart +``` diff --git a/.claude/skills/ato-website-build-deploy/scripts/build-web.sh b/.claude/skills/ato-website-build-deploy/scripts/build-web.sh new file mode 100755 index 0000000..8cd8569 --- /dev/null +++ b/.claude/skills/ato-website-build-deploy/scripts/build-web.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# build-web.sh — reproduce the CI production web build locally. +# +# Mirrors .github/workflows/main.yaml steps 3-6 (pub get -> gen-l10n -> +# analyze errors-only -> build web --release -t lib/main_production.dart). +# Output lands in build/web (gitignored). It does NOT deploy. +# +# Usage: ./scripts/build-web.sh +# Run from anywhere; the script cd's to the repo root (two levels above +# .claude/skills/ato-website-build-deploy/scripts/). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# scripts/ -> ato-website-build-deploy/ -> skills/ -> .claude/ -> repo root +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)" +cd "${REPO_ROOT}" + +# This repo has no system Flutter; the pinned SDK is driven by fvm (.fvmrc=3.41.6). +if ! command -v fvm >/dev/null 2>&1; then + echo "ERROR: fvm not found on PATH. Install fvm (e.g. 'brew install fvm')." >&2 + echo "The website pins Flutter 3.41.6 via .fvmrc; do not use a bare 'flutter'." >&2 + exit 1 +fi + +echo "==> Flutter SDK (expect 3.41.6 / Dart 3.11.4):" +fvm flutter --version | head -1 + +echo "==> [1/4] fvm flutter pub get" +fvm flutter pub get + +echo "==> [2/4] fvm flutter gen-l10n" +fvm flutter gen-l10n + +echo "==> [3/4] fvm flutter analyze (errors only, matches CI)" +fvm flutter analyze --no-fatal-warnings --no-fatal-infos + +echo "==> [4/4] fvm flutter build web --release -t lib/main_production.dart" +fvm flutter build web --release -t lib/main_production.dart + +echo "==> Done. Artifact in ${REPO_ROOT}/build/web (gitignored)." +echo " To ship it manually: firebase deploy --only hosting (deploys LIVE)." +echo " Normally push to master (live) or open a PR (preview) instead." diff --git a/.claude/skills/ato-website-content-and-brand/SKILL.md b/.claude/skills/ato-website-content-and-brand/SKILL.md new file mode 100644 index 0000000..4854b50 --- /dev/null +++ b/.claude/skills/ato-website-content-and-brand/SKILL.md @@ -0,0 +1,220 @@ +--- +name: ato-website-content-and-brand +description: >- + Use when editing marketing copy, hero/service/teaser text, or case studies on + the Anaheim Technologies website; adding a new page or nav link; changing the + brand colors, fonts, wordmark, or the cold-start splash screen; questions + about "why is the splash green different from the app green", the Mechsuit / + Plavsky fonts, or where a piece of on-page text lives; or deciding what to do + about the scaffolded-but-unused Spanish (es) localization. +--- + +# ATO Website — Content & Brand + +The marketing site for **Anaheim Technologies (ATO)** — a self-described +"specialist product studio, small by design, senior by default." This skill is +the home for the site's *domain*: the marketing copy, the case studies, the +brand system (colors / fonts / splash), and the recipes for editing them. It is +about **what the site says and how it looks**, not how it is wired. + +Repo root (all paths below are relative to it): +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website` + +## What the site is (positioning, verified 2026-07-07) + +- **Three services**, presented two different ways (see the asymmetry note + below): Branding · Product Design/Development/Prototyping · Virtual Teams. +- **"CURRENTLY BUILDING: Loooans"** teaser on the home page — a fintech product + in closed beta, used as social proof ("we dogfood every stack we recommend"). +- **Two case studies** on `/projects`: `BPV Loan Monitoring` and + `[PROTOTYPE] HLP Systems`. +- **Primary CTA** everywhere: booking link `https://bit.ly/CallATO` + (`lib/features/home/screen/home_screen_content.dart:179`). Socials: + `fb.com/anaheim.technologies`, `linkedin.com/company/anaheim-technologies`. +- **Tagline / voice:** "Ideas — Delivered". Also in the `` and Open + Graph / Twitter meta in `web/index.html:22-47`. + +## When NOT to use this skill (use a sibling in THIS repo instead) + +| Your task | Go to | +|-----------|-------| +| How routing / `ShellRoot` / `MasterDetailScreen` / the responsive-globals pattern actually *work* | **ato-website-architecture** | +| Build/run commands, fvm, Firebase Hosting deploy, how a copy change ships live | **ato-website-build-deploy** | +| Removing the empty `es` l10n, dead counter scaffold, or adding real widget tests | **ato-website-testing-and-cleanup** | +| The contact form → Firestore `inquiries` → email pipeline | **ato-website-inquiries-and-integration** | + +This skill tells you *what copy to change and where the brand tokens are*; it +points to **ato-website-architecture** for the widget mechanics behind each +recipe rather than re-explaining them. + +## The content model — where copy lives + +**All real copy is hardcoded English string literals inside widgets.** There is +no CMS and l10n is effectively unused (see "l10n reality" below). To change text +you edit the Dart widget, then rebuild (`fvm flutter build web ...`, see +ato-website-build-deploy). No `.arb` regeneration is involved. + +| Page / route | File | What copy lives here | +|--------------|------|----------------------| +| `/` home | `lib/features/home/screen/home_screen_content.dart` | Hero sentence (`:22`), "Ideas -\nDelivered" tagline (`:34`,`:46`), the 3-way service blurb `switch` (`:130-137`), "Are you ready to co-create…" (`:154`), "CURRENTLY BUILDING / Loooans" teaser (`:207`,`:216`,`:226`), footer | +| `/services` | `lib/features/services/screen/services_screen.dart` | Four long narratives declared as locals (`brandingNarrative`, `pddNarrative`, `prototypingNarrative`, `virtualTeamsNarrative`, `:21-45`) + `titleList` (`:52-57`) | +| `/projects` | `lib/features/projects/screen/projects_screen.dart` | `titleList` (`:22-24`), `narrativeList` (`:26-40`), `detailList` images/SVG (`:42-77`) | +| `/us` about | `lib/features/about_us/screen/about_us_screen.dart` | Bio `Text.rich` (`:29-41`) + the 14-logo tech `Wrap` (`:68-82`) | +| `/contact` | `lib/features/contact_us/screen/contact_us_screen.dart` | Form labels/CTA copy (form *behaviour* → ato-website-inquiries-and-integration) | +| `/privacy` | `lib/features/privacy/screen/privacy_screen.dart` | Thin Data-Privacy-Act-of-2012 notice | +| Nav labels & footer | `lib/features/home/screen/home_screen.dart` | Menu items ("about us / services / projects / contact us"), footer ATO wordmark + social icons | +| Splash, SEO, OG/Twitter meta, page `<title>` | `web/index.html` | See brand section | + +Full copy inventory with every string anchor: `references/content-map.md`. + +> **Content asymmetry to know before touching services.** The home page service +> selector has **3** buttons (`Branding` / `Product Design, Development and +> Prototyping` / `Virtual Teams`, `home_screen_content.dart:292-373`) but the +> `/services` page splits them into **4** items (`Branding` / `Product design +> and development` / `Prototyping` / `Virtual teams`). Labels also differ in +> casing/wording between the two. If you rename a service, update **both** so +> the story stays consistent. + +## Brand system + +Colors live in one place: `lib/utils/color_utils.dart` (class `AppColors`). + +| Token | Hex | Role | +|-------|-----|------| +| `gradientTop` | `#1B282F` | Page background gradient, top (`:6`) | +| `gradientBottom` | `#0D161A` | Page background gradient, bottom (`:7`); also used as on-green text color | +| `textLinkColor` | `#41F39E` | **Signature green** — buttons, links, accents, hover indicators (`:8`) | +| `foregroundColor` | `#D3D3D3` | Default body text / icon tint (`:4`) | +| `whiteColor` | `Colors.white` | Wordmark + emphasis (`:5`) | + +**Fonts** (declared in `pubspec.yaml:42-48`): + +| Family | Source | Used for | +|--------|--------|----------| +| **Noto Sans** | `google_fonts` at runtime (`app/view/app.dart:119`, `GoogleFonts.notoSansTextTheme()`) | Base body text theme | +| **Mechsuit** | bundled `fonts/Mechsuit.otf` | "ATO" wordmark + section headers ("Projects", "Services", "About us") | +| **Plavsky** | bundled `fonts/Plavsky.otf` | Taglines & accents ("Ideas - Delivered", "Loooans", nav labels; the `HoveredText` default, `widget_utils.dart:96`) | + +Fonts are applied by literal `fontFamily: 'Mechsuit'` / `'Plavsky'` strings in +~17 spots — no central text-style constants. Grep before assuming a rename is +one edit: `grep -rn "fontFamily: '" lib/`. + +**Two brand nits (labeled, do not "fix" silently):** + +1. **Splash green ≠ app green.** The cold-start splash uses `#3EE58C` + (`web/index.html:87`,`:95`) while the app's signature green is `#41F39E` + (`color_utils.dart:8`). Known cosmetic inconsistency. +2. **Vestigial blue accent.** `MaterialApp`'s theme still carries the Very Good + CLI default `Color(0xFF13B9FF)` for `appBarTheme` / `colorScheme.accentColor` + (`app/view/app.dart:115-117`). The site renders no `AppBar`, so this blue is + effectively dead; the real accent is `textLinkColor`. + +Deeper brand reference (splash CSS anatomy, the font-path trick, google_fonts +runtime behaviour): `references/brand-and-splash.md`. + +## The splash screen + +Pure HTML/CSS injected into `web/index.html:50-119` (added in website-repo +commit `95e1d9d`, "add branded splash screen for cold-start gap"). It covers the +~1–3 s blank cold-start gap before Flutter paints its first frame. + +- A fixed `#ato-splash` overlay on `#0A0A0A` shows "ATO" (Mechsuit, `:107`), an + animated green pulse bar (`:108`), and "Ideas — Delivered" (Plavsky, `:109`). +- It **fades out and self-removes** when the browser fires the + `flutter-first-frame` event (`:112-118`). +- Its `@font-face` rules load the bundled OTFs from `assets/fonts/Mechsuit.otf` + / `assets/fonts/Plavsky.otf` (`:51-60`). **There is no `assets/fonts/` + directory in source** — that path is where Flutter's web build serves any + pubspec-declared asset. `asset: fonts/Mechsuit.otf` in `pubspec.yaml` becomes + bundle path `fonts/Mechsuit.otf`, served on web under the `assets/` prefix → + `assets/fonts/Mechsuit.otf`. Do not "correct" the splash path to `fonts/…`. + +To edit the splash: change the markup/CSS directly in `web/index.html`. It is +plain HTML — no Dart, no rebuild-of-Flutter needed beyond the normal web build. + +## Recipes + +### Edit hero / service / teaser copy +1. Find the string in the table above (or `grep -rn "phrase" lib/`). +2. Edit the literal in place. Watch for `''' … '''` raw multiline strings + (services narratives, projects narratives) and `switch` arms + (`home_screen_content.dart:130-137`). +3. Rebuild & preview per **ato-website-build-deploy**. + +### Add or edit a case study on `/projects` +Case studies are **three parallel index-aligned lists** on the shared +`MasterDetailScreen` (`projects_screen.dart`): `titleList[i]`, +`narrativeList[i]`, `detailList[i]` must all describe the same project at the +same index. To add one, append one entry to **each** of the three lists (a +title string, a narrative `'''…'''`, and a detail `Widget` — an `Image.asset` +or `SvgPicture.asset` from `assets/`). Add the image/SVG under `assets/images/` +or `assets/svg/` (already globbed by `pubspec.yaml:38-41`). For how +`MasterDetailScreen` renders and toggles these → **ato-website-architecture**. + +### Add a new page / route +This skill owns the **content** half; the wiring half is +**ato-website-architecture**. Content steps: +1. Create the screen widget under `lib/features/<name>/screen/` with your copy. +2. Add its nav label in **both** menus in `home_screen.dart._buildMenu`: the + expanded `HoveredText(text: …, navigateTo: '/<path>')` row and the compact + `PopupMenuButton` `PopupMenuItem`. +3. Add a `MenuSelectionCubit` selector + `isXSelected` getter + (`lib/features/home/cubit/menu_selection_cubit.dart`) so the nav highlight + works, and call it from the menu `onTap`/`onSelected`. + +For the `GoRoute`/`ShellRoute` registration and the `buildPageWithDefaultTransition` +wrapper → **ato-website-architecture**. + +### Change or add a brand color / font +- **Color:** add/edit a `static const` in `AppColors` + (`lib/utils/color_utils.dart`); reference it as `AppColors.<name>`. If it is + used in the splash, mirror the hex into `web/index.html` by hand (the two are + not linked — this is why the greens drifted). +- **Font:** drop the OTF in `fonts/`, declare it under `pubspec.yaml` `fonts:`, + and (for the splash) add a matching `@font-face` in `web/index.html`. Apply + with `fontFamily: '<Family>'`. + +### Edit the splash +Edit `web/index.html:50-119` directly (markup + inline CSS). Keep the +`flutter-first-frame` listener intact or the splash never dismisses. + +## l10n reality (scaffolded, effectively unused — removable debt) + +`l10n.yaml` + `flutter_localizations` are wired (en + es, `generate: true`), and +generated `app_localizations*.dart` files are committed under `lib/l10n/arb/`. +**But the only translated key is `counterAppBarTitle`** (Very Good CLI +boilerplate) in both `app_en.arb` and `app_es.arb` — no real page copy is +localized, and nothing in the UI reads a localized string for content. The +Spanish (`es`) locale is untranslated in substance. + +Per the maintainer (2026-07-07): the site is **web-only for good** and the empty +`es` l10n is **removable debt**. Treat it as safe-to-delete unless localization +is deliberately revived. The removal runbook (and the CI test-step decision) +lives in **ato-website-testing-and-cleanup** — do the actual deletion there, not +here. + +## Provenance and maintenance + +Authored 2026-07-07 from direct read-only inspection of the repo at +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website` +(website repo, branch `master`). Binding maintainer decisions: web-only is +permanent; empty `es` l10n is removable debt (human-answers A2). The splash was +added in commit `95e1d9d` (website repo). + +Re-verify volatile facts (run from repo root): + +```bash +# Brand color tokens (expect #1B282F, #0D161A, #41F39E, #D3D3D3) +grep -n "Color(0xFF" lib/utils/color_utils.dart +# Splash green + the green mismatch (expect #3EE58C in index.html vs #41F39E above) +grep -n "3EE58C\|#0A0A0A\|flutter-first-frame" web/index.html +# Bundled fonts declared (expect Plavsky + Mechsuit from fonts/) +sed -n '42,48p' pubspec.yaml +# l10n is still boilerplate-only (expect just counterAppBarTitle) +cat lib/l10n/arb/app_en.arb +# One-shot check of all of the above: +bash .claude/skills/ato-website-content-and-brand/scripts/verify-brand.sh +``` + +If `color_utils.dart` gains new tokens, the splash hex changes, or `app_en.arb` +grows real keys, update this skill and `references/brand-and-splash.md`. diff --git a/.claude/skills/ato-website-content-and-brand/references/brand-and-splash.md b/.claude/skills/ato-website-content-and-brand/references/brand-and-splash.md new file mode 100644 index 0000000..2cac076 --- /dev/null +++ b/.claude/skills/ato-website-content-and-brand/references/brand-and-splash.md @@ -0,0 +1,113 @@ +# Brand & splash — deep reference + +Verified 2026-07-07 against the website repo. + +## Color tokens (`lib/utils/color_utils.dart`) + +```dart +class AppColors { + static const foregroundColor = Color(0xFFD3D3D3); // body text / icon tint + static const whiteColor = Colors.white; // wordmark, emphasis + static const gradientTop = Color(0xFF1B282F); // bg gradient top + static const gradientBottom = Color(0xFF0D161A); // bg gradient bottom + on-green text + static const textLinkColor = Color(0xFF41F39E); // SIGNATURE GREEN — links, buttons, accents +} +``` + +The dark background gradient (`gradientTop` → `gradientBottom`) is applied by +`WidgetUtils.defaultBackground` (`lib/utils/widget_utils.dart`) and used by +`HomeScreen` as the page chrome. `textLinkColor` is the one brand accent you +will reach for constantly — filled buttons, outlined-button borders/foreground, +hover underlines, the green SVG tint on `/projects`. + +There is **no** dedicated "brand palette" object beyond this class, and no +semantic naming beyond these five. Contrast/accessibility of `#41F39E` on the +dark gradient has not been formally audited (note for a future design pass). + +## Fonts + +Three families in play: + +1. **Noto Sans** — the base text theme, fetched at runtime by the `google_fonts` + package: `GoogleFonts.notoSansTextTheme()` in `lib/app/view/app.dart:119`, + re-colored to `AppColors.foregroundColor`. By default `google_fonts` + downloads the font from Google's CDN on first paint and caches it, so the + base font is a soft **network dependency** (it will fall back to a system + sans if offline). This is stock Very Good CLI behaviour, not a deliberate + choice; if bundling is ever wanted, `google_fonts` supports shipping the OTF + as an asset instead. +2. **Mechsuit** (`fonts/Mechsuit.otf`, ~19 KB) — the "ATO" wordmark and section + headers. +3. **Plavsky** (`fonts/Plavsky.otf`, ~34 KB) — taglines and accent text. + +Both OTFs are declared in `pubspec.yaml:42-48`: + +```yaml +fonts: + - family: Plavsky + fonts: + - asset: fonts/Plavsky.otf + - family: Mechsuit + fonts: + - asset: fonts/Mechsuit.otf +``` + +Applied via literal `fontFamily: 'Mechsuit'` / `'Plavsky'` strings (no central +constants). As of 2026-07-07, `Mechsuit` appears in 5 widget files and +`Plavsky` in 4 (incl. the `HoveredText` default style, +`widget_utils.dart:96`). Confirm before a rename: `grep -rn "fontFamily: '" lib/`. + +### Why the splash references `assets/fonts/` when there is no such source dir + +Source layout has the OTFs at repo-root `fonts/`, and there is **no** +`assets/fonts/` directory in the repo. Yet the splash `@font-face` loads +`assets/fonts/Mechsuit.otf`. This is correct, because: + +- A pubspec `asset: fonts/Mechsuit.otf` becomes the **bundle asset path** + `fonts/Mechsuit.otf` (verified in a local build's + `build/flutter_assets/FontManifest.json`: family `Mechsuit` → `fonts/Mechsuit.otf`). +- Flutter's **web** build serves every bundled asset under the `assets/` + prefix, so at runtime the file is reachable at `assets/fonts/Mechsuit.otf`. + +So the raw HTML splash and the Flutter app end up loading the *same* physical +OTF from the same served URL. **Do not "fix" the splash path to `fonts/…`** — it +would 404 on the deployed site. + +## Splash screen anatomy (`web/index.html:50-119`) + +Added in website-repo commit `95e1d9d` ("add branded splash screen for +cold-start gap"). Purpose: mask the ~1–3 s white gap before Flutter's first +frame on a cold load. + +Structure: + +- `<style>` block (`:50-102`): two `@font-face` rules (`:51-60`), then the + `#ato-splash` overlay and its children. + - `#ato-splash` — `position: fixed; inset: 0; z-index: 9999;` on `#0A0A0A`, + centered column (`:61-73`). + - `.ato-fade-out` — `opacity: 0` transition class toggled on dismiss (`:74-77`). + - `.ato-mark` — "ATO" in Mechsuit, 56 px, white, letter-spaced (`:78-83`). + - `.ato-pulse` — a 60×2 px bar in **`#3EE58C`** with the `ato-pulse` keyframe + scaleX animation (`:84-90`, keyframes `:98-101`). + - `.ato-tagline` — "Ideas — Delivered" in Plavsky, uppercase, **`#3EE58C`** + (`:91-97`). +- `<body>` markup (`:106-110`): the `#ato-splash` div with the three children. +- Dismiss script (`:111-119`): on the `flutter-first-frame` window event, add + `ato-fade-out`, then `splash.remove()` after 400 ms (matching the 0.4 s CSS + transition). + +### The green mismatch (known nit) + +Splash accent `#3EE58C` (`:87`, `:95`) ≠ app signature green `#41F39E` +(`color_utils.dart:8`). The two are maintained independently — the splash is raw +HTML, the app reads Dart constants — so they drifted. If you unify them, change +the splash hex to `#41F39E` in **both** `web/index.html:87` and `:95`. Leave it +labeled as a nit otherwise; it is cosmetic and low priority. + +### Editing rules + +- Editing the splash is a plain HTML/CSS edit in `web/index.html`; no Dart. +- **Keep the `flutter-first-frame` listener** (`:112`). If you remove or break + it, the overlay never dismisses and the site appears stuck on the splash. +- If you add a new font to the splash, add a matching `@font-face` and rely on + the same `assets/fonts/<File>.otf` served path (see above). diff --git a/.claude/skills/ato-website-content-and-brand/references/content-map.md b/.claude/skills/ato-website-content-and-brand/references/content-map.md new file mode 100644 index 0000000..3600d7d --- /dev/null +++ b/.claude/skills/ato-website-content-and-brand/references/content-map.md @@ -0,0 +1,91 @@ +# Content map — every on-page string and where it lives + +Verified 2026-07-07 against the website repo. All copy is hardcoded English +string literals in Dart widgets (or in `web/index.html` for the shell). There is +no CMS and no live l10n. Line anchors are stable-ish but re-grep the phrase if a +number looks off. + +## `/` — home (`lib/features/home/screen/home_screen_content.dart`) + +| Copy | Anchor | +|------|--------| +| Hero: "We turn ideas into shipped products — web, mobile, cloud, and hardware…" | `:22` | +| Tagline "Ideas -\nDelivered" (Plavsky, rendered twice: expanded `:34`, compact `:46`) | `:34`, `:46` | +| "Our services" / "Contact us" buttons | `:68`, `:90` | +| Service selector buttons: "Branding" `:313`, "Product Design, Development and Prototyping" `:340`, "Virtual Teams" `:367` | `:292-373` | +| Service blurb `switch` (state 0 branding / 1 product / 2 virtual teams) | `:130-137` | +| "Are you ready to co-create with us?" | `:154` | +| "We are excited to work with you!" | `:167` | +| "Talk to us today" booking button → `https://bit.ly/CallATO` | `:179`,`:195` | +| "CURRENTLY BUILDING" eyebrow | `:207` | +| "Loooans" (Plavsky) | `:216` | +| "The first product on our fintech stack. Now in closed beta.\nWe dogfood every stack we recommend." | `:226` | +| Footer: "ATO" wordmark (Mechsuit) + Facebook/LinkedIn SVG links | `:243-280` | + +The service selector state is driven by `ServiceSelectCubit` (0=branding, +1=pdd, 2=vt). The blurb text is a separate `switch` on the same state. + +## `/services` (`lib/features/services/screen/services_screen.dart`) + +Four narrative locals, then fed to `MasterDetailScreen`: + +| Narrative local | Anchor | +|-----------------|--------| +| `pddNarrative` — "Imagine an idea sitting in a notes app…" | `:21-26` | +| `prototypingNarrative` — "Some products can't live entirely on a screen…" (mentions GlacierGrid, YC-backed IoT, LoRaWAN) | `:28-33` | +| `virtualTeamsNarrative` — "Building a team is hard…" | `:35-40` | +| `brandingNarrative` — "Your brand is the first thing customers see…" | `:42-45` | +| `titleList` = Branding / Product design and development / Prototyping / Virtual teams | `:52-57`, `:73-78` | + +Note the compact (`!Constants.isExpandedScreen`) and expanded branches build the +same content two different ways (`:47-67` vs `:69-110`) — edit both if you +restructure. + +## `/projects` (`lib/features/projects/screen/projects_screen.dart`) + +Three index-aligned lists on `MasterDetailScreen` — item `i` in each describes +the same case study: + +| List | Content | Anchor | +|------|---------|--------| +| `titleList` | `'BPV Loan Monitoring'`, `'[PROTOTYPE] HLP Systems'` | `:22-24` | +| `narrativeList` | BPV description (Baybay Property Ventures Corp, loan/lot monitoring); HLP description (collections/remittances, "Integrity, Accountability and Security") | `:26-40` | +| `detailList` | `[0]` stacked screenshots `assets/images/pic1..3.png`; `[1]` `assets/svg/3d-model.svg` tinted green | `:42-77` | + +Known copy typos present in source (leave unless doing a deliberate copy pass): +"andhow" (`:32`), "application andhow we can help" — verbatim in the narrative. + +## `/us` — about (`lib/features/about_us/screen/about_us_screen.dart`) + +| Copy | Anchor | +|------|--------| +| Bio `Text.rich`: "Anaheim Technologies is a specialist product studio — small by design, senior by default…" (13+ yrs, ex-GlacierGrid, Loooans, "even this website is written in Flutter") | `:29-41` | +| Inline "Talk to us today!" link → `/contact` | `:47-55` | +| Tech-logo `Wrap` — 14 SVGs from `assets/svg/` | `:68-82` | + +The 14 logos, in order: flutter, dart, firebase, gcp (full), golang, java +(full), kotlin, nodejs, python, swift, typescript, cpp, gcp_iot_core, +lorawan (dark). Rendered at `height: 56`. + +## `/contact` (`lib/features/contact_us/screen/contact_us_screen.dart`) + +Booking button + name/email/message form. Copy lives here; the form's +write-to-Firestore behaviour is documented in +**ato-website-inquiries-and-integration**, not here. + +## `/privacy` (`lib/features/privacy/screen/privacy_screen.dart`) + +Short Data-Privacy-Act-of-2012 (Philippines) notice. + +## Shell / SEO copy (`web/index.html`) + +| Copy | Anchor | +|------|--------| +| `<meta name="description">` — "Anaheim Technologies is a specialist product studio…" | `:22` | +| Open Graph title/description/url/image | `:25-29` | +| Twitter card title/description/url/image | `:32-36` | +| `<title>` "Anaheim Technologies — Ideas, Delivered" | `:47` | +| Splash markup "ATO" / pulse / "Ideas — Delivered" | `:106-110` | + +If you rebrand the tagline or company blurb, update the on-page Dart copy **and** +these meta/OG/Twitter tags so shares and search results match. diff --git a/.claude/skills/ato-website-content-and-brand/scripts/verify-brand.sh b/.claude/skills/ato-website-content-and-brand/scripts/verify-brand.sh new file mode 100755 index 0000000..f96658d --- /dev/null +++ b/.claude/skills/ato-website-content-and-brand/scripts/verify-brand.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Re-verify the ATO website brand & content facts asserted by +# ato-website-content-and-brand. Read-only. Run from anywhere. +# +# bash .claude/skills/ato-website-content-and-brand/scripts/verify-brand.sh +# +# Prints each asserted fact and whether it still holds. Exit 0 if all pass. +set -u + +# Resolve repo root = two levels up from this script's .claude/skills/<skill>/scripts dir. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +cd "$REPO_ROOT" || { echo "cannot cd to repo root $REPO_ROOT"; exit 2; } + +fail=0 +check() { # desc, pattern, file + local desc="$1" pat="$2" file="$3" + if grep -Eq "$pat" "$file" 2>/dev/null; then + printf 'OK %s\n' "$desc" + else + printf 'DRIFT %s (pattern /%s/ not found in %s)\n' "$desc" "$pat" "$file" + fail=1 + fi +} + +echo "== Brand color tokens (lib/utils/color_utils.dart) ==" +check "gradientTop #1B282F" '0xFF1B282F' lib/utils/color_utils.dart +check "gradientBottom #0D161A" '0xFF0D161A' lib/utils/color_utils.dart +check "signature green #41F39E" '0xFF41F39E' lib/utils/color_utils.dart +check "foreground #D3D3D3" '0xFFD3D3D3' lib/utils/color_utils.dart + +echo "== Splash (web/index.html) ==" +check "splash overlay #0A0A0A" '#0A0A0A' web/index.html +check "splash green #3EE58C (green nit)" '3EE58C' web/index.html +check "dismiss on flutter-first-frame" 'flutter-first-frame' web/index.html +check "splash loads assets/fonts/*.otf" 'assets/fonts/.*\.otf' web/index.html + +echo "== Bundled fonts (pubspec.yaml) ==" +check "Mechsuit from fonts/Mechsuit.otf" 'fonts/Mechsuit\.otf' pubspec.yaml +check "Plavsky from fonts/Plavsky.otf" 'fonts/Plavsky\.otf' pubspec.yaml + +echo "== Base font (lib/app/view/app.dart) ==" +check "Noto Sans via google_fonts" 'notoSansTextTheme' lib/app/view/app.dart + +echo "== l10n still boilerplate-only (lib/l10n/arb/app_en.arb) ==" +if grep -q 'counterAppBarTitle' lib/l10n/arb/app_en.arb 2>/dev/null \ + && [ "$(grep -c '"@' lib/l10n/arb/app_en.arb 2>/dev/null)" -le 2 ]; then + printf 'OK app_en.arb holds only the counter boilerplate key\n' +else + printf 'DRIFT app_en.arb changed — real l10n keys may now exist; revisit the l10n section\n' + fail=1 +fi + +echo "== Key content anchors present ==" +check "CTA https://bit.ly/CallATO" 'bit\.ly/CallATO' lib/features/home/screen/home_screen_content.dart +check "CURRENTLY BUILDING / Loooans teaser" 'CURRENTLY BUILDING' lib/features/home/screen/home_screen_content.dart +check "case study BPV Loan Monitoring" 'BPV Loan Monitoring' lib/features/projects/screen/projects_screen.dart +check "case study [PROTOTYPE] HLP Systems" '\[PROTOTYPE\] HLP Systems' lib/features/projects/screen/projects_screen.dart + +echo +if [ "$fail" -eq 0 ]; then + echo "All brand/content facts verified." +else + echo "One or more facts drifted — update SKILL.md + references/ accordingly." +fi +exit "$fail" diff --git a/.claude/skills/ato-website-inquiries-and-integration/SKILL.md b/.claude/skills/ato-website-inquiries-and-integration/SKILL.md new file mode 100644 index 0000000..b9eb678 --- /dev/null +++ b/.claude/skills/ato-website-inquiries-and-integration/SKILL.md @@ -0,0 +1,188 @@ +--- +name: ato-website-inquiries-and-integration +description: >- + Use when working on the contact/inquiry form's Firestore write, the + {prefix}inquiries collection or its dev_/prod ENVIRONMENT prefix, debugging why + a contact-form submission did or did not send a notification email, the + cross-repo handoff to the functions repo's inquiryCreated trigger, or the + Firestore inquiries security rules (create-only posture, spam hardening, + exporting console-managed rules into source). +--- + +# ATO website — inquiries pipeline & integration (website side) + +This is the **primary home** for the contact-form → email pipeline **as seen from +this Flutter web repo**, plus the Firestore `inquiries` security posture and the +runbook to pull the console-managed rules into source. + +The site's contact form does exactly ONE backend thing: it **writes a Firestore +document**. It sends no email itself. A **separate repo's** Firestore trigger +turns that write into email. Understand this boundary before touching anything. + +## The pipeline (end to end) + +``` +[Browser: /contact form] (this repo) + └─ ContactUsBloc.sendEmail() -> SendEmailEvent + └─ FirebaseFirestore.instance + .collection('${prefix}inquiries').add({...}) <- the ONLY backend call the site makes + (prefix = 'dev_' iff --dart-define=ENVIRONMENT=development, else '') + | + v +[Firestore, project anaheim-technologies] + collection "inquiries" <- PROD path: a doc-create here fires the trigger + collection "dev_inquiries" <- DEV path: NO trigger watches this -> dead end (by design) + | + v (server-side Firestore document-create event; browser is NOT involved) +[Functions repo: ../anaheim_technologies_website_functions] (SEPARATE repo) + inquiryCreated = onDocumentCreated('inquiries/{docId}') + └─ POST -> sendMail (Microsoft Graph) -> confirmation + notification emails +``` + +Because the browser only ever talks to **Firestore** (never to the Cloud +Function), **CORS is a non-issue** on this path. The function is reached +server-to-server by Firestore's trigger, not by the web client. + +The email/function half lives in a **different repo** and is documented there. +See "When NOT to use this skill" below. Do not try to load that repo's skills +from here — they are a different `.claude/skills` tree. + +## The write contract (the website↔functions interface) + +Source of truth: `lib/features/contact_us/bloc/contact_us_bloc.dart` (verified +2026-07-07). The `.add()` call at lines 45-52 writes these fields: + +| Firestore field | Written from (this repo) | Type | Consumed by trigger? | +| --------------- | -------------------------------------------- | ---- | -------------------- | +| `email_address` | `event.email` | str | yes -> `emailAddress` | +| `full_name` | `event.fullName` | str | yes -> `fullName` | +| `message` | `event.message` | str | yes -> `messageRaw` / template | +| `inquired_on` | `DateTime.now().millisecondsSinceEpoch` | int | **no** (written, ignored) | + +The functions repo destructures `email_address`, `full_name`, `message` in +`functions/src/index.ts` (`inquiryCreated`). **These snake_case field names are a +contract.** If you rename a field here, the email breaks silently (the trigger +reads `undefined`), because the failure is server-side and the user already saw a +success state after the Firestore write. Change both repos together, and keep the +names snake_case. + +`inquired_on` is a client-clock epoch-millis int, not a Firestore server +timestamp — fine for display/sort, but do not treat it as trustworthy time. + +For **how the bloc and the form widget are wired into the app** (bloc states, +the `/contact` route, responsive layout), that is app architecture — see the +sibling skill `ato-website-architecture`, not here. + +## Prefix / environment reality (why prod works, dev is a dead end) + +The collection name is `'${_firestore_collection_prefix}inquiries'`. The prefix +getter (`contact_us_bloc.dart:26-34`): + +```dart +String get _firestore_collection_prefix { + const env = String.fromEnvironment('ENVIRONMENT'); + if (env == 'development') return 'dev_'; + return ''; +} +``` + +- **Production** ships via CI: `.github/workflows/main.yaml` runs + `flutter build web --release -t lib/main_production.dart` with **no** + `--dart-define=ENVIRONMENT`. So `ENVIRONMENT` is empty -> prefix `''` -> + writes plain **`inquiries`** -> the trigger fires -> email sends. This works. +- **Development** runs (e.g. the IntelliJ config, or a manual + `fvm flutter run ... --dart-define=ENVIRONMENT=development`) write + **`dev_inquiries`**. + +**The dev gap is intentional (maintainer decision A4, 2026-07-07).** The +`inquiryCreated` trigger only watches `inquiries/{docId}`, and there is **no dev +Firebase project** (single project `anaheim-technologies`, `.firebaserc`). So a +development build's inquiries land in `dev_inquiries`, which **no trigger reads**, +so **no email is ever sent for dev-build submissions**. This is by design — the +sanctioned dev path for exercising the email flow is the **functions emulator** +(owned by the functions repo), not the live project. Do NOT "fix" this by +pointing dev at `inquiries` or by adding a `dev_inquiries` trigger; that would +send real email from local/test submissions. Leave it as-is. + +For the single-project, prod-only environment model and how prod builds ship, the +primary home is the sibling skill `ato-website-build-deploy`. + +## Security posture — OPEN, and thin (maintainer decision A3) + +As of 2026-07-07, per the maintainer: + +- The Firestore rules for `inquiries` are **create-only** and **have NO spam / + abuse protection** (no App Check, no rate limit, no captcha, no auth + requirement on the create). +- They are **managed only in the Firebase console**. There is **no + `firestore.rules` in this repo** and **no `firestore` block in `firebase.json`** + (verified: `firebase.json` is hosting-only; no tracked `firestore.rules`). + +This is an **open posture, not a solved problem.** Anyone can create `inquiries` +docs at will; the only backstop downstream is that the function sends a canned +email. Treat "harden this" as pending work, and never describe the current rules +as if they were adequate. + +Two follow-ups, both **candidate / open** (do not present as done): + +1. **Get the rules into source** so they are reviewable and versioned. Runbook: + `references/rules-export-runbook.md`. Helper: `scripts/fetch-live-firestore-rules.sh`. +2. **Add spam/abuse hardening** once rules are in source: Firebase App Check on + the web app (note: `firebase_app_check` is **not** currently a dependency — + `pubspec.yaml` has `cloud_firestore` + `firebase_core` only, so this needs a + new package + registration), and/or a rules condition constraining field + shape/size. Candidates are enumerated in `references/rules-export-runbook.md`. + Do not implement silently — these change who can write and require the + maintainer's call. + +To confirm what the live rules actually are before you touch them (this repo +cannot tell you — they are console-side), see the verification path in +`references/rules-export-runbook.md`. + +## When NOT to use this skill + +| You are working on... | Go to | +| ------------------------------------------------------------------ | ----- | +| The email send, Microsoft Graph, `sendMail`, or the `inquiryCreated` trigger's internals / security | The **functions repo** at `../anaheim_technologies_website_functions` (its own `.claude/skills`) — a **separate** repo, not a sibling skill here | +| How `ContactUsBloc` / the `/contact` form is structured in the app (bloc states, routing, responsive layout) | `ato-website-architecture` | +| Build/run/CI/deploy mechanics, the single-project prod-only model, how prod builds ship | `ato-website-build-deploy` | +| Writing tests for the contact form / adding a CI test step | `ato-website-testing-and-cleanup` | +| Editing marketing copy on the `/contact` page | `ato-website-content-and-brand` | + +The functions repo is a **different git repo**. When you need its side of the +contract, open its files directly by path +(`../anaheim_technologies_website_functions/functions/src/index.ts`); you cannot +`load` its skills from this repo. + +## Provenance and maintenance + +Authored 2026-07-07 from direct inspection of this repo and the sibling functions +repo. Binding maintainer decisions: A3 (create-only console rules, no spam +protection, document + export runbook) and A4 (single prod project; +`dev_inquiries` gap is by design; emulators are the dev path). + +Repo-of-origin note for cited history (website repo, shallow): the contact write +was added in `ae33364` ("added contact us functionality to send email"); the +`inquired_on` field in `d1daf29` ("added inquire_on date when saving user +inquiries"). + +Re-verify volatile facts: + +```bash +# The write contract (fields, collection, prefix logic): +sed -n '26,52p' lib/features/contact_us/bloc/contact_us_bloc.dart + +# Prod CI still builds main_production without --dart-define=ENVIRONMENT: +grep -n "build web\|dart-define\|ENVIRONMENT" .github/workflows/main.yaml + +# Still no firestore.rules / firestore block in source, single project: +ls firestore.rules 2>/dev/null || echo "still console-only" +grep -n firestore firebase.json || echo "firebase.json still hosting-only" +cat .firebaserc # expect single default: anaheim-technologies + +# The trigger still only watches unprefixed 'inquiries' (other repo): +grep -n "onDocumentCreated" ../anaheim_technologies_website_functions/functions/src/index.ts + +# What the LIVE console rules actually are (see references for auth caveats): +scripts/fetch-live-firestore-rules.sh anaheim-technologies +``` diff --git a/.claude/skills/ato-website-inquiries-and-integration/references/rules-export-runbook.md b/.claude/skills/ato-website-inquiries-and-integration/references/rules-export-runbook.md new file mode 100644 index 0000000..4133a5d --- /dev/null +++ b/.claude/skills/ato-website-inquiries-and-integration/references/rules-export-runbook.md @@ -0,0 +1,142 @@ +# Runbook: export the console-managed `inquiries` rules into source, then harden + +As of 2026-07-07 the Firestore security rules for `inquiries` are **create-only, +have no spam/abuse protection, and live only in the Firebase console** (there is +no `firestore.rules` in this repo and no `firestore` block in `firebase.json`). +This is maintainer decision A3 and it is an **open** posture, not a finished one. + +This runbook does two separable things: +1. **Read** the live rules and commit them into this repo as source of truth + (low risk; changes nothing that is deployed). +2. **Harden** them (candidates only; each one changes who can write — the + maintainer decides, and any change must be deployed deliberately). + +Do step 1 before step 2 — you cannot review or safely edit rules you have not +first captured as text. + +--- + +## Step 0 — know your project and account + +- Firebase project: **`anaheim-technologies`** (single project; `.firebaserc` + has only `default`). There is no dev/staging project. +- The account that reads/deploys rules must have access to that project. Note + the known gotcha: your `gcloud` CLI account and your Application Default + Credentials (ADC) account can differ, and project access does not carry across + them. If a read 403s, check *which* identity the token belongs to + (`gcloud auth list`, `gcloud config get-value account`) before assuming you + lack access. + +--- + +## Step 1a — READ the live rules (pick one) + +**Option A — Firebase console (simplest, zero tooling, always works).** +Firebase console -> Firestore Database -> **Rules** tab. The full rules text is +shown there. This is the current source of truth; copy it verbatim. + +**Option B — REST API via a gcloud token** (`scripts/fetch-live-firestore-rules.sh`). +The Firebase CLI has **no** `firestore:rules get` subcommand (verified against +firebase-tools 14.27.0 on 2026-07-07 — the only `firestore:*` verbs are +`delete`, `indexes`, `databases:*`, `backups:*`, etc.). The documented way to +fetch the *active* ruleset programmatically is the Firebase Security Rules REST +API (`firebaserules.googleapis.com`): + +``` +GET /v1/projects/{project}/releases/cloud.firestore -> { rulesetName: "projects/.../rulesets/UUID" } +GET /v1/projects/{project}/rulesets/{UUID} -> source.files[].content (the rules text) +``` + +The helper script wraps both calls. It needs `gcloud auth print-access-token` +and `jq`. **UNVERIFIED against the live project during authoring** (setting an +active project / minting a live token was out of scope) — the endpoint shapes are +from Google's public Firebase Security Rules API docs. Run it, then eyeball the +output before trusting it; if it errors, fall back to Option A. + +**Do NOT use `firebase init firestore` to "download" the rules.** It scaffolds +`firestore.rules` + `firestore.indexes.json` and rewrites `firebase.json` +interactively, and can overwrite with a template default rather than the live +rules. Capture the text first (A or B), then create the file by hand (Step 1b). + +--- + +## Step 1b — commit the rules as source of truth + +1. Create `firestore.rules` at the repo root and paste the exact live rules text. + Expect something close to create-only for `inquiries`, e.g.: + + ``` + rules_version = '2'; + service cloud.firestore { + match /databases/{database}/documents { + match /inquiries/{docId} { + allow create: if true; // <- open create, no spam protection (the A3 reality) + allow read, update, delete: if false; + } + } + } + ``` + + Paste what the console/API actually returns — do not assume the block above is + byte-identical to production. It illustrates the *shape* to expect. + +2. Wire it into `firebase.json` (currently hosting-only). Add a sibling block: + + ```json + "firestore": { "rules": "firestore.rules" } + ``` + + (Add `"indexes": "firestore.indexes.json"` too only if you also capture + indexes; not required for a rules-only export.) + +3. Commit. This is now the reviewable source of truth. **This alone deploys + nothing.** + +### Deploy safety after wiring firestore into firebase.json + +- **CI is safe.** This repo's CI (`.github/workflows/main.yaml`) deploys via + `FirebaseExtended/action-hosting-deploy@v0`, which pushes **hosting only** — it + never runs `firebase deploy`, so it will not push rules. +- **A local bare `firebase deploy` is NOT safe** once the `firestore` block + exists: it would deploy hosting **and** firestore rules together. To push rules + on purpose and only rules: `firebase deploy --only firestore:rules`. Never let + a rules deploy be an accident. +- Decide deliberately whether rules become **source-managed going forward**. If + yes, the repo is now authoritative and console edits will be clobbered by the + next `firebase deploy --only firestore:rules`; keep future rule changes in the + repo. If you only wanted a versioned snapshot, say so in the commit message and + keep deploying rules from the console. + +--- + +## Step 2 — hardening candidates (OPEN — maintainer's call, deploy deliberately) + +All of these are **candidates**, listed so a future engineer does not have to +rediscover them. None is implemented. Each changes who can write `inquiries`, so +each needs the maintainer's decision and a deliberate `--only firestore:rules` +(and, for App Check, app-side wiring) deploy. Verify the contact form still +submits after any of them. + +1. **Field-shape constraint in the rule.** Tighten `allow create` to require the + exact contract and cap sizes, e.g. only the four known keys, each a string + (except `inquired_on` an int), with `message` length-bounded. Cheap, no new + dependency, blocks garbage/oversized docs. Does not stop a determined bot. + +2. **Firebase App Check on the web app + `allow create: if request.auth != null` + is NOT the right lever here** (the form is anonymous). App Check is: it + attests the request came from your real web app (reCAPTCHA provider on web), + and rules can require `request.appCheck` context. **Cost:** add the + `firebase_app_check` package (not currently in `pubspec.yaml` — today it is + just `cloud_firestore` + `firebase_core`), register App Check in web init, + register the site key in the console, and enforce on Firestore. Highest + effort, strongest bot resistance. + +3. **A captcha / challenge in the form** before the write — client-side friction; + weaker than App Check and adds UI work, but no rules change. Lowest-trust. + +4. **Rate limiting** is not expressible in Firestore rules alone; it would need a + counter doc + rule, or moving the create behind an authenticated/App-Check'd + callable. Note it as harder; do not pretend a rule can do per-IP limiting. + +Recommended sequencing if hardening is greenlit: (1) field-shape rule first +(cheap, immediate), then (2) App Check for real bot resistance. diff --git a/.claude/skills/ato-website-inquiries-and-integration/scripts/fetch-live-firestore-rules.sh b/.claude/skills/ato-website-inquiries-and-integration/scripts/fetch-live-firestore-rules.sh new file mode 100755 index 0000000..ccafbc4 --- /dev/null +++ b/.claude/skills/ato-website-inquiries-and-integration/scripts/fetch-live-firestore-rules.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Fetch the LIVE Firestore security rules for a Firebase project and print the +# rules text to stdout. Read-only: it only performs GETs against the Firebase +# Security Rules REST API (firebaserules.googleapis.com). +# +# Why this exists: firebase-tools has NO `firestore:rules get` subcommand +# (verified against firebase-tools 14.27.0, 2026-07-07), and `firebase init +# firestore` can overwrite local rules with a template. This wraps the two +# documented REST calls instead. +# +# CAVEAT (2026-07-07): the endpoint shapes are from Google's public Firebase +# Security Rules REST API docs; this script was NOT executed against the live +# project during authoring (no active project / live token in scope). Eyeball the +# output; if it errors or looks wrong, use the console Rules tab instead +# (references/rules-export-runbook.md, Option A). +# +# Requires: gcloud (authenticated with access to the project) and jq. +# +# Usage: +# scripts/fetch-live-firestore-rules.sh [PROJECT_ID] +# Default PROJECT_ID: anaheim-technologies (this repo's single project). + +set -euo pipefail + +PROJECT="${1:-anaheim-technologies}" +API="https://firebaserules.googleapis.com/v1" + +command -v gcloud >/dev/null 2>&1 || { echo "error: gcloud not found on PATH" >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "error: jq not found on PATH" >&2; exit 1; } + +TOKEN="$(gcloud auth print-access-token)" || { + echo "error: could not mint an access token. Check 'gcloud auth list' and that" >&2 + echo " the active account has access to project '$PROJECT'." >&2 + exit 1 +} + +echo ">> account: $(gcloud config get-value account 2>/dev/null) project: $PROJECT" >&2 + +# 1. Resolve the active ruleset name from the cloud.firestore release. +RELEASE_JSON="$(curl -fsS -H "Authorization: Bearer $TOKEN" \ + "$API/projects/$PROJECT/releases/cloud.firestore")" || { + echo "error: failed to read release cloud.firestore for '$PROJECT' (403 => wrong identity/access)." >&2 + exit 1 +} + +RULESET_NAME="$(printf '%s' "$RELEASE_JSON" | jq -r '.rulesetName')" +if [ -z "$RULESET_NAME" ] || [ "$RULESET_NAME" = "null" ]; then + echo "error: no rulesetName in release response:" >&2 + printf '%s\n' "$RELEASE_JSON" >&2 + exit 1 +fi +echo ">> active ruleset: $RULESET_NAME" >&2 + +# 2. Fetch that ruleset and print each source file's content. +RULESET_JSON="$(curl -fsS -H "Authorization: Bearer $TOKEN" "$API/$RULESET_NAME")" || { + echo "error: failed to fetch ruleset $RULESET_NAME" >&2 + exit 1 +} + +printf '%s' "$RULESET_JSON" | jq -r '.source.files[].content' diff --git a/.claude/skills/ato-website-testing-and-cleanup/SKILL.md b/.claude/skills/ato-website-testing-and-cleanup/SKILL.md new file mode 100644 index 0000000..75e423e --- /dev/null +++ b/.claude/skills/ato-website-testing-and-cleanup/SKILL.md @@ -0,0 +1,254 @@ +--- +name: ato-website-testing-and-cleanup +description: >- + Use when writing, running, or wiring in Flutter tests for this marketing site + (widget/integration tests, responsive/mobile/small-screen coverage, + `flutter test`, coverage); when the broken counter tests fail or you notice CI + never runs tests; when the coverage badge or MIT badge looks wrong; or when + deciding whether to delete the counter scaffold, empty Spanish l10n, + android/ios/windows scaffolds, orphaned assets, or stale Dependabot branches. +--- + +# ATO website: testing and cleanup + +This skill covers two linked jobs for the `anaheim_technologies_website` Flutter +web app: + +1. **Get to a real test suite** — the current `test/` tree is broken Very Good + CLI boilerplate; replace it with widget tests that MUST cover + responsive/mobile/small-screen layouts, then wire `flutter test` into CI. +2. **Work the sanctioned cleanup backlog** — the maintainer has decided this app + is **web-only for good** (see `human-answers.md` A2, 2026-07-07). The + counter scaffold, empty Spanish l10n, mobile/Windows scaffolds, and stale + branches are removable debt. This skill lists them as a **decide-then-do + checklist**, never silent deletion. + +Verified against the repo on **2026-07-07**. Re-verify with the one-liners in +"Provenance and maintenance" before trusting anything volatile. + +## When NOT to use this skill (use a sibling instead) + +- **How the toolchain / build / CI / deploy works** (fvm 3.41.6, `fvm flutter`, + the CI workflow steps, Firebase Hosting) → `ato-website-build-deploy`. This + skill only *adds a test step* to that CI; it does not own the toolchain. +- **The architecture you are testing** — GoRouter/ShellRoute, the + responsive-globals pattern, blocs/cubits, Firestore contact flow → + `ato-website-architecture`. This skill cites the globals only as far as + needed to test them; the primary explanation lives there. +- **Content/brand/how-to-add-a-page** → `ato-website-content-and-brand`. +- **The contact-form → Firestore → functions email pipeline and inquiries + rules** → `ato-website-inquiries-and-integration`. + +--- + +## Part 1 — Current testing state (as of 2026-07-07) + +`test/` is stale Very Good CLI scaffold and **broken**. Do not trust it. + +| File | Problem | +| --- | --- | +| `test/app/view/app_test.dart` | Asserts `find.byType(CounterPage)` (line 9). `App` renders a `GoRouter`/`HomeScreen` (`lib/app/view/app.dart:94-96`, initial route `/` → `HomeScreenContent`) and never a `CounterPage`. **This test fails if run.** | +| `test/counter/cubit/counter_cubit_test.dart` | Tests `lib/counter/cubit/counter_cubit.dart` — **dead scaffold**, referenced nowhere in `lib/` except itself (grep confirms no `CounterPage`/`CounterCubit` use in `main_*.dart`, `bootstrap.dart`, or `lib/app/`). | +| `test/counter/view/counter_page_test.dart` | Same — tests the dead `lib/counter/view/counter_page.dart`. | +| (everything real) | **Zero tests** for home, services, projects, contact form, or routing. | + +Why nobody notices: **CI never runs `flutter test`.** The workflow +(`.github/workflows/main.yaml`) runs `pub get` → `gen-l10n` → `flutter analyze +--no-fatal-warnings --no-fatal-infos` → `flutter build web` → deploy. No test +step. + +Two cosmetic lies that follow from this: + +- `coverage_badge.svg` reads **100%** — stale/meaningless (nothing generates it). +- README shows an **MIT license badge** (`README.md:5`, `:160-161`) but there is + **no `LICENSE` file** in the repo. + +### Run the (currently broken) suite once to see the failures + +The test helper `test/helpers/pump_app.dart` imports `AppLocalizations`, so the +generated l10n must exist first. Use the wrapper script or: + +```sh +fvm flutter pub get +fvm flutter gen-l10n +fvm flutter test # counter + app tests FAIL today — expected +``` + +(`fvm` and the pinned SDK are owned by `ato-website-build-deploy`.) + +--- + +## Part 2 — Add real tests (the forward-looking core) + +**Maintainer requirement (A2):** widget/integration tests **MUST cover +responsive/mobile-view/small-screen layouts.** Widget tests satisfy this — they +run headless and let you set the surface size. You do not need a browser for +responsive coverage. + +### The one thing you must understand before writing a responsive test + +This app decides layout from **mutable static globals**, not from `MediaQuery` +at the point of use: + +- Breakpoints live in `lib/utils/screen_utils.dart:26-29`: + `< 600 → compact`, `< 840 → medium`, else `expanded`. +- Those feed three globals in `lib/utils/constants.dart:5-7`: + `Constants.isCompactScreen` / `isMediumScreen` / `isExpandedScreen` + (default: compact `true`, others `false`). +- The globals are **only recomputed inside `App.build`** + (`lib/app/view/app.dart:103-108`, which reads `MediaQuery`). +- Feature widgets read the globals **directly** — e.g. + `home_screen_content.dart:29,42`, `home_screen.dart:120`, + `contact_us_screen.dart:73,131` all branch on `Constants.isExpandedScreen`. + (Architecture rationale: `ato-website-architecture`.) + +**Consequence for tests:** setting the surface size alone does **not** update +the globals unless `App.build` runs. A widget-level test that pumps one page in +isolation MUST set the globals explicitly, or it will silently test the default +(compact) layout no matter what width you gave it. + +The helper below does both — set surface size AND recompute the globals from the +same breakpoints — so your tests can't drift out of sync. + +### Copy this helper into `test/helpers/responsive_pump.dart` + +The full, ready-to-paste helper and two worked test skeletons (a phone width +`< 600` and a desktop width, asserting compact-vs-expanded layout on a real +page) are in **`references/testing-guide.md`**. The core idea: + +```dart +Future<void> pumpAtWidth( + WidgetTester tester, + Widget child, { + required double width, + double height = 900, +}) async { + // 1. Set the real surface size (for widgets that DO read MediaQuery). + tester.view.physicalSize = Size(width, height); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + // 2. Recompute the static globals from the SAME breakpoints the app uses + // (screen_utils.dart) — feature widgets read these, not MediaQuery. + Constants.isCompactScreen = width < 600; + Constants.isMediumScreen = width >= 600 && width < 840; + Constants.isExpandedScreen = width >= 840; + + await tester.pumpWidget(MaterialApp(home: child)); + await tester.pump(); +} +``` + +Then assert the responsive difference. A robust, high-value assertion for +mobile coverage is **"no overflow at phone width"** — a `RenderFlex` overflow +surfaces as an exception you can catch: + +```dart +testWidgets('home lays out without overflow at phone width', (tester) async { + await pumpAtWidth( + tester, + BlocProvider(create: (_) => ServiceSelectCubit(), child: HomeScreenContent()), + width: 375, + ); + expect(tester.takeException(), isNull); // catches RenderFlex overflow +}); +``` + +Notes that matter (details + full desktop-vs-phone example in the guide): + +- `HomeScreenContent` needs a `ServiceSelectCubit` provider (it uses + `BlocBuilder<ServiceSelectCubit, int>`, `home_screen_content.dart:111,120`). +- Prefer **widget-level** pumping (plain `MaterialApp`) over pumping the whole + `App()`. `App` installs a `GoogleFonts.notoSansTextTheme()` theme + (`app.dart:119`) which tries a runtime font fetch under test; a plain + `MaterialApp` sidesteps it. If you must pump `App()`, set + `GoogleFonts.config.allowRuntimeFetching = false` first. +- `integration_test` is **not** a dependency yet (`pubspec.yaml` dev deps: + `bloc_test`, `mocktail`, `very_good_analysis`, `flutter_test` only). Adding + browser-driven end-to-end tests requires adding it — see the guide. + +### Replace the broken counter tests + +Delete `test/counter/` and `test/app/view/app_test.dart` (or rewrite the app +test to assert `find.byType(HomeScreen)` instead of `CounterPage`). Keep +`test/helpers/pump_app.dart` — it is reusable. This is coupled to removing +`lib/counter/*` in the cleanup backlog below; do both together. + +### Wire `flutter test` into CI — ONLY once real tests exist + +Do **not** add the step while the suite is still the broken counter scaffold — +it will fail every build. Once real tests are green, add a step to +`.github/workflows/main.yaml` **after "Analyze" and before "Build web"**: + +```yaml + - name: Test + run: flutter test --coverage +``` + +Exact placement and rationale (the analyze gate, why after analyze) are in +`references/testing-guide.md`. CI mechanics beyond this one step belong to +`ato-website-build-deploy`. + +--- + +## Part 3 — Sanctioned cleanup backlog (A2: web-only for good) + +**Decide-then-do, not silent deletion.** Each item is real debt the maintainer +has greenlit removing, but confirm the current state (evidence commands in +`references/cleanup-backlog.md`) before you delete, and land each as its own +reviewable change. Run `scripts/audit-cleanup.sh` for a read-only status pass. + +| # | Item | Action | Evidence (verified 2026-07-07) | +| --- | --- | --- | --- | +| 1 | `lib/counter/*` dead scaffold + its tests | Delete `lib/counter/`, `test/counter/`, fix/remove `test/app/view/app_test.dart` | No refs in `lib/` outside `lib/counter/` (l10n `counterAppBarTitle` string is unrelated) | +| 2 | Empty Spanish l10n | Delete `app_es.arb` + regen, **or** actually translate | `lib/l10n/arb/app_es.arb` has only `counterAppBarTitle`; all UI copy is hardcoded English | +| 3 | Vestigial `android/`, `ios/`, `windows/` scaffolds | Trim to web-only | `firebase_options.dart` is web-only (android/iOS/macOS/windows/linux all `throw UnsupportedError`, `:24-49`); only Hosting deploys; README "works on iOS, Android, Web, and Windows" (`README.md:34`) is CLI boilerplate | +| 4 | 5 stale Dependabot branches + `origin/versions/2` | Prune remote branches; bump `go_router` off `^6.5.2` deliberately | `git branch -r`: `dependabot/pub/{bloc_test-9.1.3, flutter_bloc-8.1.3, go_router-10.0.0, url_launcher-6.1.11, very_good_analysis-4.0.01}` + `versions/2`; `pubspec.yaml:22` pins `go_router: ^6.5.2` | +| 5 | Orphaned assets | Delete `assets/under-construction*.png` | Referenced only in a commented-out attribution (`app.dart:129-130`); no code use | +| 6 | Committed hosting cache | `git rm --cached` + gitignore | `.firebase/hosting.YnVpbGQvd2Vi.cache` is tracked | +| 7 | Unused imports (the analyze debt) | Remove them | `intl` unused in `constants.dart:1`; `dart:math as math` and `url_launcher_string` unused in both `services_screen.dart` and `projects_screen.dart` (`:7,:10`) | +| 8 | Missing LICENSE vs MIT badge | Add a real `LICENSE` **or** drop the badge | README claims MIT (`:5,:160-161`); no `LICENSE` file | + +Item 4's branches are on the **remote** — pruning needs `git push origin +--delete <branch>` and is a mutating action; get maintainer sign-off. Item 3 is +the largest blast radius (whole directories) — do it last and confirm nothing in +CI or `pubspec.yaml` assets references those trees. + +Full per-item runbook, exact commands, and the "why this is safe" reasoning: +**`references/cleanup-backlog.md`**. + +--- + +## Provenance and maintenance + +Authored **2026-07-07** from direct inspection of +`/Users/deibeeed/Projects/AnaheimTechnologies/ato_website/anaheim_technologies_website` +(no CLAUDE.md/MEMORY.md in this repo — these skills are the persistent +knowledge). Cross-repo functions live at a separate path +(`../anaheim_technologies_website_functions`) and are not loadable from here. + +Re-verify volatile facts (run from repo root): + +```sh +# Test tree + broken counter assertion still present? +find test -type f +grep -n "find.byType(CounterPage)" test/app/view/app_test.dart + +# Dead counter scaffold still referenced nowhere real? +grep -rn "CounterPage\|CounterCubit" lib/main_*.dart lib/bootstrap.dart lib/app/ + +# CI still has no test step? (expect: build/analyze/deploy, no "flutter test") +grep -n "flutter test" .github/workflows/main.yaml || echo "no test step (as documented)" + +# Breakpoints + globals unchanged? +grep -n "deviceWidth <" lib/utils/screen_utils.dart +grep -n "isExpandedScreen\|isMediumScreen\|isCompactScreen" lib/utils/constants.dart + +# Cleanup backlog still open? +bash .claude/skills/ato-website-testing-and-cleanup/scripts/audit-cleanup.sh +``` + +Open items stay **open** until the maintainer lands them. Nothing here is +oversold: the responsive test approach is verified against real widget code, but +the exact CI test step and any `integration_test` browser command must be +proven green in CI before you rely on them (labeled in the guide). diff --git a/.claude/skills/ato-website-testing-and-cleanup/references/cleanup-backlog.md b/.claude/skills/ato-website-testing-and-cleanup/references/cleanup-backlog.md new file mode 100644 index 0000000..535e7c1 --- /dev/null +++ b/.claude/skills/ato-website-testing-and-cleanup/references/cleanup-backlog.md @@ -0,0 +1,197 @@ +# Cleanup backlog — per-item runbook + +Depth for `SKILL.md` Part 3. The maintainer decided this app is **web-only for +good** (`human-answers.md` A2, 2026-07-07): the counter scaffold, empty Spanish +l10n, mobile/Windows scaffolds, and stale branches are removable debt; real +tests are wanted. Everything below is **decide-then-do** — confirm current state +with the evidence command, get sign-off for anything mutating a shared remote, +and land each item as its own reviewable change. Verified 2026-07-07. + +`scripts/audit-cleanup.sh` runs the read-only checks for items 1-7 in one pass. + +--- + +## 1. Dead `lib/counter/*` scaffold + its tests + +**State:** `lib/counter/counter.dart`, `lib/counter/cubit/counter_cubit.dart`, +`lib/counter/view/counter_page.dart` exist but are referenced nowhere real. + +**Verify:** +```sh +grep -rn "CounterPage\|CounterCubit\|counter/counter.dart" lib/main_*.dart lib/bootstrap.dart lib/app/ +# expect: no matches. (The l10n string "counterAppBarTitle" is unrelated copy.) +``` + +**Do:** delete `lib/counter/`, `test/counter/`, and fix or delete +`test/app/view/app_test.dart` (it asserts `find.byType(CounterPage)` and will +not compile once `lib/counter/counter.dart` is gone). Do this in the same change +as Part 2's "replace the broken counter tests." Keep `test/helpers/`. + +--- + +## 2. Empty Spanish l10n + +**State:** `lib/l10n/arb/app_es.arb` contains only the boilerplate +`counterAppBarTitle` key; all real UI copy is hardcoded English string literals +in widgets. `l10n.yaml` sets `arb-dir: lib/l10n/arb`, template `app_en.arb`. + +**Verify:** +```sh +cat lib/l10n/arb/app_es.arb # only counterAppBarTitle +grep -rn "l10n\." lib/features/ # near-zero real localized lookups +``` + +**Do (pick one):** +- Drop it: delete `app_es.arb`, remove `es` from supported locales, re-run + `fvm flutter gen-l10n`. Simplest, matches "web-only, English marketing site." +- Revive it: only if Spanish is actually wanted — then real copy must move out of + widget literals into ARB keys first (large content refactor; coordinate with + `ato-website-content-and-brand`). + +Default recommendation: drop, since no copy is localized. + +--- + +## 3. Vestigial `android/`, `ios/`, `windows/` scaffolds + +**State:** full Very Good CLI platform scaffolds exist, but the app only ships +to Firebase Hosting (web). `firebase_options.dart` supports **web only** — every +other platform throws. + +**Verify:** +```sh +grep -n "UnsupportedError" lib/firebase_options.dart # android/iOS/macOS/windows/linux +grep -n "iOS, Android, Web, and Windows" README.md # boilerplate claim, README:34 +ls -d android ios windows 2>/dev/null +``` + +**Do:** remove the unused platform trees (`android/`, `ios/`, `windows/`, and +`macOS`/`linux` if present) and correct the README's "works on iOS, Android, +Web, and Windows" line to web-only. **Largest blast radius — do it last.** +Before deleting, confirm nothing in `pubspec.yaml` `flutter:` assets, the CI +workflow, or launcher-icon config depends on those directories. This is purely a +source-tree cleanup; it does not touch the deployed site (CI builds +`flutter build web`). + +--- + +## 4. Stale branches + `go_router` pin + +**State (as of 2026-07-07):** `git branch -r` shows 5 abandoned Dependabot +branches plus `origin/versions/2`: + +``` +origin/dependabot/pub/bloc_test-9.1.3 +origin/dependabot/pub/flutter_bloc-8.1.3 +origin/dependabot/pub/go_router-10.0.0 +origin/dependabot/pub/url_launcher-6.1.11 +origin/dependabot/pub/very_good_analysis-4.0.01 +origin/versions/2 +``` + +`pubspec.yaml:22` pins `go_router: ^6.5.2` while the open Dependabot branch +proposes 10.0.0 (~2 major versions behind). + +**Verify:** +```sh +git branch -r +grep -n "go_router" pubspec.yaml +``` + +**Do:** +- Pruning remote branches is **mutating a shared remote** — get maintainer + sign-off, then: `git push origin --delete dependabot/pub/bloc_test-9.1.3` + (repeat per branch), and decide `versions/2` separately (it is 0 ahead / 9 + behind master per the dossier — likely safe to delete, but confirm with + `git log origin/master..origin/versions/2`). +- Bump `go_router` deliberately (not by merging the stale bot PR): update the + pin, run `fvm flutter pub get`, migrate any breaking API in `router_utils.dart` + / `app.dart`, and verify with the analyze gate + the new tests. `go_router` + 6→10 has breaking changes; treat as a real migration, not a lockfile bump. +- These skills do **not** perform git mutations autonomously; propose and let the + maintainer run them. + +--- + +## 5. Orphaned `assets/under-construction*.png` + +**State:** `assets/under-construction.png` and `assets/under-construction-2.png` +are unreferenced by any code. + +**Verify:** +```sh +grep -rn "under-construction" lib/ web/ pubspec.yaml +# only hits: a commented-out flaticon attribution at lib/app/view/app.dart:129-130 +``` + +**Do:** delete both PNGs. Optionally also remove the dead attribution comment at +`app.dart:129-130`. Assets are declared broadly (`assets:` includes `assets/`), +so no pubspec edit is required. + +--- + +## 6. Committed Firebase hosting cache + +**State:** `.firebase/hosting.YnVpbGQvd2Vi.cache` is tracked in git — deploy +cruft that should never be committed. + +**Verify:** +```sh +git ls-files | grep "\.firebase/" +``` + +**Do:** `git rm --cached .firebase/hosting.YnVpbGQvd2Vi.cache` and add +`.firebase/` to `.gitignore`. (Hosting/deploy mechanics: `ato-website-build-deploy`.) + +--- + +## 7. Unused imports (the analyze debt) + +**State:** the CI analyze gate was relaxed to `--no-fatal-warnings +--no-fatal-infos` to tolerate pre-existing unused imports rather than fix them +(`ato-website-build-deploy` owns the gate's history). Directly confirmed unused: + +- `import 'package:intl/intl.dart';` in `lib/utils/constants.dart:1` (class has + only three bools). +- `import 'dart:math' as math;` and + `import 'package:url_launcher/url_launcher_string.dart';` in **both** + `lib/features/services/screen/services_screen.dart:7,10` and + `lib/features/projects/screen/projects_screen.dart:7,10`. + +**Verify (authoritative list):** +```sh +fvm flutter analyze # lists every unused_import / warning +``` + +**Do:** remove the unused imports. Once the tree is clean you *could* tighten the +analyze gate back toward warnings-fatal — but that is the build-deploy skill's +call, not this one's. Also note the non-standard `snake_case` +`_firestore_collection_prefix` getter in `contact_us_bloc.dart:26` (an +`avoid_private_typedef_functions`/naming lint) if you do a lint sweep. + +--- + +## 8. Missing LICENSE vs MIT badge + +**State:** README shows an MIT license badge (`README.md:5`, links at +`:160-161`) but there is **no `LICENSE` file** in the repo. + +**Verify:** +```sh +ls LICENSE 2>/dev/null || echo "no LICENSE" +grep -n "License: MIT\|license_link" README.md +``` + +**Do:** either add a real MIT `LICENSE` file (maintainer's call on the actual +license) or remove the badge. Do not leave the mismatch. + +--- + +## Ordering suggestion + +1. Items 1 + 7 + 5 + 6 — low-risk, local, do first (pairs naturally with the + test cleanup in Part 2). +2. Item 2 (l10n) and item 8 (license) — small policy decisions. +3. Item 4 (branches + go_router bump) — needs remote sign-off + a real dep + migration. +4. Item 3 (platform trees) — largest blast radius, do last. diff --git a/.claude/skills/ato-website-testing-and-cleanup/references/testing-guide.md b/.claude/skills/ato-website-testing-and-cleanup/references/testing-guide.md new file mode 100644 index 0000000..d943c56 --- /dev/null +++ b/.claude/skills/ato-website-testing-and-cleanup/references/testing-guide.md @@ -0,0 +1,235 @@ +# Testing guide — worked skeletons and CI wiring + +Depth for `SKILL.md` Part 2. Verified against the repo on 2026-07-07. +`fvm`/SDK setup is owned by `ato-website-build-deploy`; the responsive-globals +architecture is owned by `ato-website-architecture`. This file is the concrete +"how to write the test" runbook. + +## 0. Prerequisites (once) + +The reusable helper `test/helpers/pump_app.dart` imports +`AppLocalizations` (`lib/l10n/l10n.dart` → `lib/l10n/arb/app_localizations.dart`). +The generated l10n files are committed today, but CI regenerates them, so make it +a habit: + +```sh +fvm flutter pub get +fvm flutter gen-l10n +fvm flutter test +``` + +## 1. The responsive-globals trap (read this before writing any layout test) + +The app does NOT read `MediaQuery` at the point of use. It reads three mutable +static booleans that are recomputed only in `App.build`: + +- `lib/utils/screen_utils.dart:23-30` — `getScreenSize` maps width to + `compact (<600) / medium (<840) / expanded`. +- `lib/app/view/app.dart:103-108` — `App.build` calls `getScreenSize(context)` + (which reads `MediaQuery`) and writes `Constants.isExpandedScreen`, + `isMediumScreen`, `isCompactScreen`. +- `lib/utils/constants.dart:5-7` — the globals. Defaults: `isCompactScreen = + true`, the other two `false`. +- Feature widgets branch on the globals directly. Confirmed call sites: + - `lib/features/home/screen/home_screen_content.dart:29` (`if (Constants.isExpandedScreen) ...[`) + - `lib/features/home/screen/home_screen_content.dart:42` (`if (!Constants.isExpandedScreen)`) + - `lib/features/home/screen/home_screen.dart:120` + - `lib/features/contact_us/screen/contact_us_screen.dart:73,131` + (`width: !Constants.isExpandedScreen ? double.infinity : 350`) + +**Failure mode if you forget:** you set `tester.view.physicalSize` to a phone +width, but because the widget under test never calls `getScreenSize` and +`App.build` never ran, `Constants.isExpandedScreen` keeps its previous value. +Your "phone" test silently exercises whatever layout the globals happened to +hold. Worse, the globals are process-global and leak between tests. So: **set the +globals explicitly, and reset them.** + +## 2. Full helper — `test/helpers/responsive_pump.dart` + +```dart +import 'package:anaheim_technologies_website/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Pumps [child] at a given logical [width], setting BOTH the real surface +/// size (for any widget that reads MediaQuery) AND the mutable layout globals +/// in Constants (which most feature widgets read instead of MediaQuery). +/// +/// Mirrors the breakpoints in lib/utils/screen_utils.dart (<600 / <840). +/// Tear-downs reset the view and the globals so tests don't leak into each +/// other. +Future<void> pumpAtWidth( + WidgetTester tester, + Widget child, { + required double width, + double height = 900, +}) async { + tester.view.physicalSize = Size(width, height); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + Constants.isCompactScreen = width < 600; + Constants.isMediumScreen = width >= 600 && width < 840; + Constants.isExpandedScreen = width >= 840; + addTearDown(() { + // Restore the source defaults (constants.dart:5-7). + Constants.isCompactScreen = true; + Constants.isMediumScreen = false; + Constants.isExpandedScreen = false; + }); + + await tester.pumpWidget(MaterialApp(home: child)); + await tester.pump(); +} +``` + +## 3. Worked test — one page at phone AND desktop width + +Target: `HomeScreenContent`. It renders the Plavsky "Ideas -\nDelivered" +tagline in **two different places** depending on width: + +- Expanded (`>= 840`): inside the top `Row`, in the same line as the hero copy + (`home_screen_content.dart:29-39`). +- Compact/medium (`< 840`): in its own full-width `SizedBox` below the row + (`home_screen_content.dart:42-51`). + +Both branches render exactly one such `Text`, so the tagline is present either +way — that makes a good smoke assertion — while the **no-overflow** check is the +real mobile-layout guard. + +`HomeScreenContent` reads `BlocBuilder<ServiceSelectCubit, int>` +(`:111,:120`), so it must be wrapped in a `ServiceSelectCubit` provider. + +```dart +import 'package:anaheim_technologies_website/features/home/cubit/service_select_cubit.dart'; +import 'package:anaheim_technologies_website/features/home/screen/home_screen_content.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/responsive_pump.dart'; + +void main() { + Widget subject() => BlocProvider( + create: (_) => ServiceSelectCubit(), + child: HomeScreenContent(), + ); + + group('HomeScreenContent responsive layout', () { + testWidgets('phone width (<600): renders, no overflow', (tester) async { + await pumpAtWidth(tester, subject(), width: 375); + + expect(tester.takeException(), isNull); // no RenderFlex overflow + expect(find.textContaining('Ideas'), findsWidgets); + }); + + testWidgets('desktop width (>=840): renders, no overflow', (tester) async { + await pumpAtWidth(tester, subject(), width: 1440); + + expect(tester.takeException(), isNull); + expect(find.textContaining('Ideas'), findsWidgets); + }); + }); +} +``` + +### Asserting the actual layout difference (optional, more brittle) + +If you want to prove the branch flipped (not just "renders"), assert on a +width-driven property. The contact page is the cleanest: the booking button's +`SizedBox` is `width: 350` when expanded and `double.infinity` when not +(`contact_us_screen.dart:73`). You can read it back: + +```dart +// After pumpAtWidth(tester, ContactUsScreen(), width: 375): +// the first booking-button SizedBox should be full width, not 350. +``` + +Keep these targeted and few — asserting exact widths couples tests to layout +constants and breaks on cosmetic tweaks. The no-overflow + presence checks give +most of the mobile-coverage value at a fraction of the brittleness. + +## 4. Gotchas that will waste your time + +- **google_fonts runtime fetch.** Pumping the whole `App()` installs + `GoogleFonts.notoSansTextTheme()` (`app.dart:119`), which attempts a network + font fetch under test. Prefer widget-level pumping with a plain `MaterialApp` + (as in the helper). If you genuinely need the full `App`, set + `GoogleFonts.config.allowRuntimeFetching = false;` in `setUpAll`. +- **Firestore is only touched on submit.** `ContactUsBloc` writes to + `FirebaseFirestore.instance` only inside `_handleSendEmailEvent` + (`contact_us_bloc.dart:44-52`), not at construction. So you can pump + `ContactUsScreen` and fill fields without a Firebase init; just don't drive + the Send button in a plain widget test (that needs a fake Firestore — see + below). +- **Static-global leakage.** Always reset the `Constants` globals (the helper + does). A test that sets `isExpandedScreen = true` and forgets to reset will + corrupt every later test in the file. +- **SVG assets.** `HomeScreenContent` loads `assets/svg/*.svg` + (`:260,:275`). Assets declared in `pubspec.yaml` are available to widget + tests via the bundle, but if a decode races the assertion, add + `await tester.pumpAndSettle()`. + +## 5. Testing the contact-form submit path (when you get there) + +To test the submit → Firestore write without hitting real Firebase, inject a +fake. `ContactUsBloc` currently reaches `FirebaseFirestore.instance` directly +(`contact_us_bloc.dart:45`), so it is not injectable as written. Two options: + +1. Add `fake_cloud_firestore` as a dev dependency and refactor `ContactUsBloc` + to accept a `FirebaseFirestore` in its constructor (defaulting to + `.instance`). Then `blocTest` the write. This is a small, worthwhile + testability refactor. +2. Test only the pure validation branch, which needs no Firestore: + `sendEmail` emits `ContactUsErrorState('Please fill up all fields')` when any + field is empty (`contact_us_bloc.dart:71-74`) — a clean `blocTest` with no + mocking. + +Start with option 2 (free, real coverage of a real branch), do option 1 when +the refactor is welcome. + +## 6. integration_test (browser end-to-end) — optional path + +`integration_test` is **not** in `pubspec.yaml` today (dev deps are +`bloc_test ^9.1.0`, `mocktail ^0.3.0`, `very_good_analysis ^3.1.0`, +`flutter_test`). Widget tests already satisfy the responsive requirement +headlessly, so add integration tests only for true end-to-end flows (real +router navigation, real Firebase against an emulator). + +To add it: + +```yaml +# pubspec.yaml dev_dependencies: + integration_test: + sdk: flutter +``` + +Put tests in `integration_test/`. On web they run in a real browser via +chromedriver, not `flutter test`. The exact invocation +(`fvm flutter drive` / `fvm flutter test integration_test -d chrome` with a +running `chromedriver`) is **UNVERIFIED** here — prove it green locally and in +CI before documenting it as fact, and coordinate the CI runner setup with +`ato-website-build-deploy`. + +## 7. CI test step — exact change + +Add to `.github/workflows/main.yaml`, **after** the `Analyze` step and +**before** `Build web (production)` — fail fast on logic before spending build +time: + +```yaml + - name: Analyze (errors only) + run: flutter analyze --no-fatal-warnings --no-fatal-infos + + - name: Test # <-- new + run: flutter test --coverage + + - name: Build web (production) + run: flutter build web --release -t lib/main_production.dart +``` + +Prerequisite: the suite must be green first (delete/rewrite the counter tests — +see the cleanup backlog). The CI runner already has the Flutter SDK from the +existing `subosito/flutter-action@v2` step, so no extra setup is needed. If you +later publish coverage, regenerate `coverage_badge.svg` from `coverage/lcov.info` +or delete the stale badge — do not leave the fake 100% badge in place. diff --git a/.claude/skills/ato-website-testing-and-cleanup/scripts/audit-cleanup.sh b/.claude/skills/ato-website-testing-and-cleanup/scripts/audit-cleanup.sh new file mode 100755 index 0000000..9c50620 --- /dev/null +++ b/.claude/skills/ato-website-testing-and-cleanup/scripts/audit-cleanup.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Read-only audit of the sanctioned cleanup backlog (testing-and-cleanup skill, +# Part 3). Reports whether each debt item is still present. Mutates nothing. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +cd "$REPO_ROOT" + +hr() { printf '%s\n' "------------------------------------------------------------"; } +echo "Cleanup-backlog audit for: $REPO_ROOT" +hr + +echo "[1] Dead counter scaffold + broken tests" +[ -d lib/counter ] && echo " -> lib/counter/ STILL PRESENT" || echo " -> lib/counter/ gone" +[ -d test/counter ] && echo " -> test/counter/ STILL PRESENT" || echo " -> test/counter/ gone" +grep -q "find.byType(CounterPage)" test/app/view/app_test.dart 2>/dev/null \ + && echo " -> test/app/view/app_test.dart STILL asserts CounterPage (broken)" \ + || echo " -> app_test.dart fixed/removed" +hr + +echo "[2] Empty Spanish l10n" +if [ -f lib/l10n/arb/app_es.arb ]; then + echo " -> app_es.arb present (only counterAppBarTitle expected)" +else + echo " -> app_es.arb gone" +fi +hr + +echo "[3] Vestigial platform scaffolds (web-only reality)" +for d in android ios windows macos linux; do + [ -d "$d" ] && echo " -> $d/ present" +done +grep -q "UnsupportedError" lib/firebase_options.dart 2>/dev/null \ + && echo " -> firebase_options.dart confirms web-only (non-web throws)" +hr + +echo "[4] Stale remote branches + go_router pin" +if git rev-parse --git-dir >/dev/null 2>&1; then + git branch -r 2>/dev/null | grep -E "dependabot|versions/2" | sed 's/^/ -> /' || echo " -> none" +else + echo " -> not a git checkout here; run 'git branch -r' in the repo" +fi +grep -n "go_router" pubspec.yaml | sed 's/^/ pin: /' +hr + +echo "[5] Orphaned under-construction assets" +ls assets/under-construction*.png 2>/dev/null | sed 's/^/ -> /' || echo " -> gone" +hr + +echo "[6] Committed Firebase hosting cache" +if git rev-parse --git-dir >/dev/null 2>&1; then + git ls-files 2>/dev/null | grep "\.firebase/" | sed 's/^/ -> tracked: /' || echo " -> not tracked" +else + ls .firebase/*.cache 2>/dev/null | sed 's/^/ -> present: /' || echo " -> none" +fi +hr + +echo "[7] Unused imports (spot check; run 'fvm flutter analyze' for full list)" +grep -q "package:intl/intl.dart" lib/utils/constants.dart 2>/dev/null \ + && echo " -> constants.dart still imports intl (unused)" +grep -q "dart:math" lib/features/services/screen/services_screen.dart 2>/dev/null \ + && echo " -> services_screen.dart still imports dart:math (unused)" +grep -q "dart:math" lib/features/projects/screen/projects_screen.dart 2>/dev/null \ + && echo " -> projects_screen.dart still imports dart:math (unused)" +hr + +echo "[8] LICENSE vs MIT badge" +[ -f LICENSE ] && echo " -> LICENSE present" || echo " -> NO LICENSE file (README claims MIT)" +hr +echo "Done. This audit is read-only. See references/cleanup-backlog.md for actions." diff --git a/.claude/skills/ato-website-testing-and-cleanup/scripts/run-tests.sh b/.claude/skills/ato-website-testing-and-cleanup/scripts/run-tests.sh new file mode 100755 index 0000000..ec728a1 --- /dev/null +++ b/.claude/skills/ato-website-testing-and-cleanup/scripts/run-tests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Run the Flutter test suite with coverage for the ATO website. +# Regenerates l10n first (test/helpers/pump_app.dart depends on AppLocalizations). +# Toolchain (fvm 3.41.6) is owned by the ato-website-build-deploy skill. +# +# NOTE (2026-07-07): the committed suite is broken counter boilerplate and WILL +# fail until you replace it (see the testing-and-cleanup skill). This script is +# the command you run once real tests exist. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +cd "$REPO_ROOT" + +if ! command -v fvm >/dev/null 2>&1; then + echo "fvm not found on PATH. See the ato-website-build-deploy skill." >&2 + exit 1 +fi + +echo "== pub get ==" +fvm flutter pub get +echo "== gen-l10n ==" +fvm flutter gen-l10n +echo "== test (coverage -> coverage/lcov.info) ==" +fvm flutter test --coverage "$@" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8215d4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# CLAUDE.md + +Guidance for Claude Code working in the **Anaheim Technologies marketing website** — a Flutter **web** app deployed to Firebase Hosting (`anaheimtechnologies.com`). + +## Skill library (read first) + +This repo has an on-demand skill library at `.claude/skills/`. Load the relevant one before non-trivial work — each SKILL.md holds the verified depth this file intentionally omits: + +| Skill | Load when | +|---|---| +| `ato-website-build-deploy` | building, running, deploying, or touching CI | +| `ato-website-architecture` | navigating/modifying `lib/`, routing, state, responsive layout | +| `ato-website-content-and-brand` | editing copy, case studies, fonts, palette, the splash screen | +| `ato-website-testing-and-cleanup` | writing tests (must cover small-screen) or clearing the web-only debt | +| `ato-website-inquiries-and-integration` | the contact form → Firestore → email pipeline and its security | + +## Always-true facts + +- **`fvm flutter` only** — Flutter is pinned to 3.41.6 via `.fvmrc`; there is no system `flutter`/`dart` on PATH. (CI is the exception — it uses `subosito/flutter-action`.) +- **Web-only.** `firebase_options.dart` is web-only; the `android/`, `ios/`, `windows/` scaffolds are vestigial (nothing builds or ships them). +- **Push to `master` auto-deploys LIVE** to Firebase Hosting (project `anaheim-technologies`); a PR against `master` deploys an ephemeral preview channel. CI has **no test step** and the analyze gate is errors-only. +- **The contact form sends no email from this repo** — `ContactUsBloc` writes one Firestore doc to `${prefix}inquiries`; a Cloud Function in the **separate** repo `../anaheim_technologies_website_functions` sends the email. `ENVIRONMENT=development` → `dev_inquiries` (no email, by design); prod builds ship empty `ENVIRONMENT` → `inquiries` (the path the trigger watches). +- **Single Firebase project, prod-only** (`anaheim-technologies`). Emulators are the dev path. + +## Quick commands + +```bash +fvm flutter pub get +fvm flutter gen-l10n +fvm flutter run -d chrome -t lib/main_development.dart --dart-define=ENVIRONMENT=development +fvm flutter build web --release -t lib/main_production.dart # what CI ships +```