Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
838f8d6
fix: stop silently substituting bids below the real minimum
claude Aug 3, 2026
67194ce
fix: UI consistency pass — un-Card-wrap dense tables, dedupe metrics/…
claude Aug 3, 2026
813a244
fix: more UI consistency — un-Card-wrap tables, token-ize hardcoded v…
claude Aug 3, 2026
1ffe450
fix: dedupe ad-hoc ID generation onto the single generateId source of…
claude Aug 3, 2026
a945dc9
fix: dedupe training-views list and KPI formulas in consoleNav.ts
claude Aug 3, 2026
bae7378
fix: remove dead state from CreateCampaignWizard
claude Aug 3, 2026
7fc10d0
perf: fix O(n·m) search-term dedup loop and memoize dashboard aggregates
claude Aug 3, 2026
77b494e
fix: replace dead Tailwind sizing classes with explicit SVG dimensions
claude Aug 3, 2026
eb6d656
Fix mojibake em-dashes/arrow in landing page copy
claude Aug 3, 2026
8d5e938
Add missing object-fit:cover rule for landing page images
claude Aug 3, 2026
e7f5520
Fix dashboard sidebar/alerts card overflowing viewport
claude Aug 3, 2026
edb2369
Wire the Sidebar into the app layout — Missions/Reports/Bulk ops/Trai…
claude Aug 3, 2026
3ca3125
Fix adjustTargetBid throwing instead of flooring on decrement
claude Aug 3, 2026
15d162e
Add engine test coverage requested in review
claude Aug 3, 2026
8c2241e
Address review nits: drop dead import, use EmptyState for no-data report
claude Aug 3, 2026
e411d36
Fix sitewide Astryx Card padding zeroed out by the global CSS reset
claude Aug 3, 2026
37aaf54
Fix wizard review step showing "Lookback" for non-audience campaigns
claude Aug 3, 2026
22058f4
Bump version to 3.6.0
claude Aug 3, 2026
2830a2f
Document 3.6.0 changes and the CSS cascade-layers gotcha
claude Aug 3, 2026
93ba857
Add visible bid validation feedback in TargetsTab; strengthen two vac…
claude Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Changelog

All notable changes to this project are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
This file starts at 3.6.0 — earlier releases were not retroactively documented.

## [3.6.0] - 2026-08-03

### Fixed

- **Sidebar navigation was never mounted.** The `Sidebar` component and its
`getLeftRail`/training-rail nav model (`nav/consoleNav.ts`) were fully
built and unit-tested but never rendered into `AdConsole.tsx` — Missions,
Reports, Bulk ops, Trainer, and Integrity were unreachable from the
desktop UI. `MobileNav` had the same gap from a different angle (its
section resolution only ever returned `campaigns`/`portfolio`). Both are
now wired through the shared `sidebarSectionForView`/`isSidebarItemActive`/
`resolveSidebarClick` helpers.
- **Every Astryx `Card`'s `padding` prop silently rendered as `0px`
sitewide.** Root cause: Astryx ships component styles inside
`@layer astryx-base`/`@layer astryx-theme`, and per the CSS
cascade-layers spec an unlayered declaration always beats a layered one
regardless of specificity. This app's global reset
(`*, *::before, *::after { ...; padding: 0; }`) was unlayered, so it
unconditionally zeroed every Astryx padding prop — this is why content
(buttons, headings, form fields) so often sat flush against card edges.
Scoped the reset down to just `ul`/`ol` (the only elements relying on it).
- Campaign creation wizard's Review & Launch step showed a "Lookback: 30
days" row on every campaign, including plain Sponsored Products, because
`audienceLookback` defaults to `'30'` regardless of type. Gated it on the
campaign's targeting mode actually being an audience mode. Also added
missing review rows for ASIN/category/audience targets and SB/SD creative
fields (headline, brand, destination) that were entered earlier in the
wizard but never shown before launch.
- `.split`'s `2fr 1fr` grid (dashboard "Operator alerts"/"Training
coverage" cards) didn't shrink to fit once the sidebar took up real
width, clipping content past the viewport edge. Fixed with
`minmax(0, ...)` tracks.
- `adjustTargetBid` (the "-10%"/"+10%" bid buttons) threw an uncaught
error when decrementing an already-cheap bid below the platform
minimum, instead of flooring it — a regression from the bid
fail-fast change below.
- `setTargetBid`/`setAdGroupDefaultBid` no longer silently substitute a
bid below the real $0.02 minimum; they fail fast via a new
`assertValidBid`/`MIN_BID` (`src/lib/validation.ts`), matching this
codebase's existing fail-fast convention. Creation/normalization paths
(`addTarget`, `normalizeCampaign`) still clamp, since those fill in
defaults for incomplete data rather than acting on explicit user intent.
- Campaign Manager's empty state no longer says "No campaigns yet" when a
search/filter simply matched nothing — it now shows a distinct "no
matches" state with a "Clear filters" action.
- Fixed a campaign-ID collision risk in `launchCampaign` (two campaigns of
the same type launched within the same millisecond could get the same
ID) by switching to the shared `generateId` helper, and deduplicated the
same ad-hoc ID-generation pattern across the `profiles`/`reports`/
`trainer`/`integrity` feature engines.
- Fixed literal mojibake (`—`, `→`) in the landing page copy and a
missing `.object-cover` rule that left two landing-page images without
`object-fit` applied.
- Replaced dead Tailwind sizing classes (`w-6 h-6`, `w-4 h-4`) on landing
page icons with explicit SVG dimensions — this repo has no Tailwind
compiler wired up, so the classes were doing nothing.

### Added

- Contract tests pinning the sidebar fix (renders, drives navigation to
the previously-unreachable views) and the wizard review-step fix.
- Regression tests for `isVideoFormat`, the shared `generateId` migration
(same-millisecond uniqueness across all four feature engines), and the
simulation's search-term dedup across repeated `simulateDays` calls.

### Changed

- Deduplicated hand-rolled metrics/formatter logic in `PortfolioOverview`,
`Dashboard`, and `CampaignManager` onto the shared `totalMetrics`/
`formatMoney`/`formatWhole`/`formatPercent` engine functions.
- Un-Card-wrapped dense tables in `PortfolioOverview`, `Dashboard`,
`OverviewTab`, `BulkOpsPage`, and `ReportsPage` per this repo's own
convention (dense data renders edge-to-edge, `Card` is for dashboard
widgets/settings groups only).
- Fixed an O(n·m) duplicate-detection loop in the search-term simulator
(now O(1) via a `Set`) and memoized a few expensive per-render
aggregations (`Dashboard`, `ManagerSearchTermsTab`).
- Removed 13 dead `useState` hooks and unused imports from
`CreateCampaignWizard`.
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Core engine (src/engine/ad-console/core/) — zero framework dependencies, pure
**`core/engine/`, `core/types.ts`, and `core/simulation.ts` have zero React/Next/Zustand dependencies.** They are pure TypeScript: given state in, return new state out, no mutation, no side effects. This is the most important invariant in the codebase — it's what makes those modules portable and unit-testable in isolation. Never import React, Next.js, or store code into them. Note that `core/slices/` (below) is the one exception within `core/` — it depends on Zustand's `StateCreator` type by design, since its job is to wrap the pure engine in store slices.

- `core/types.ts` — every domain interface (Campaign, AdGroup, Target, Negative, BudgetRule, Portfolio, Metrics, etc.)
- `core/engine/` — one module per domain concern: `campaign.ts`, `target.ts`, `adgroup.ts`, `negative.ts`, `budget.ts`, `portfolio.ts`, `draft.ts`, `id.ts`, `metrics.ts`, `responsive.ts`, `search-term-generator.ts`. All re-exported through `core/engine/index.ts`.
- `core/engine/` — one module per domain concern: `campaign.ts`, `target.ts`, `adgroup.ts`, `negative.ts`, `budget.ts`, `portfolio.ts`, `draft.ts`, `id.ts`, `metrics.ts`, `responsive.ts`, `search-term-generator.ts`. All re-exported through `core/engine/index.ts`. `campaign.ts` also exports `isVideoFormat(type, adFormat)`, the single source of truth for which `adFormat` string means "video" for a given campaign type (SB uses `'Video'`, SD uses `'Video creative'`) — used by both the engine and `OverviewTab`.
- `core/simulation.ts` — the 7-day performance simulator; metrics cascade target → ad group → campaign → dashboard.
- `core/slices/` — Zustand-dependent `StateCreator` slices (core, target, adgroup, negative, budget, portfolio, draft) that wrap the pure engine functions with state.
- `features/<name>/` — self-contained modules (`drills`, `profiles`, `trainer`, `bulk`, `reports`, `missions`, `integrity`), each with its own `types.ts`, `engine.ts`, `store.ts`. Adding a feature means adding a new directory here — existing files shouldn't need edits (open/closed).
Expand Down Expand Up @@ -86,13 +86,15 @@ NextAuth v5 (beta), Credentials provider, JWT sessions, bcrypt password hashing.

### UI conventions (Astryx design system)
Components come from `@astryxdesign/core` (153 components, theme via `@astryxdesign/theme-neutral`). This is actively used across the component tree (~40 files) — don't hand-roll layout `<div>`s or raw CSS when an Astryx component/prop/token covers it. Key rules (full detail lives in `AGENTS.md`'s Astryx block):
- No raw `<div>` for layout — components handle layout/spacing (`AppShell` for full pages, `SideNav` for sidebar nav).
- No raw `<div>` for layout — components handle layout/spacing (`AppShell` for full pages, `SideNav` for sidebar nav). In practice the component layer predates full Astryx adoption and still uses hand-rolled `.app-layout`/`.app-sidebar`/`.app-main` divs throughout (see "known SOLID violations" below) — match the existing pattern in a file rather than mixing conventions mid-component.
- Dense data → `Table`/`List`/`Item` rows edge-to-edge, never Card-wrapped. `Card` is for dashboard widgets/galleries/settings groups only.
- Styling values must be tokens (`var(--color-*|--spacing-*|--radius-*)`) — no raw hex/px, no Tailwind utility classes (this repo has no Tailwind compiler wired up despite Tailwind appearing in some older docs).
- Discover components/props via the CLI: `npm run astryx -- component <Name>`, `npm run astryx -- search "<thing>"`, `npm run astryx -- build "<idea>"`.
- **Never add `padding` (or any box-model property Astryx components expose as a prop) to a bare-selector reset in `globals.css`** (e.g. `*, *::before, *::after { ... }`). Astryx ships its component styles inside `@layer astryx-base`/`@layer astryx-theme`; per the CSS cascade-layers spec, *any* unlayered declaration beats a layered one regardless of specificity. An unlayered `* { padding: 0 }` silently zeroed every Astryx `padding` prop sitewide until it was found and fixed (3.6.0) — the global reset only zeroes `margin`, plus `padding` on the couple of native elements (`ul`, `ol`) that actually need it. If a future reset-like rule needs to beat Astryx's own styling, put it in the unlayered `src/app/astryx-theme.css` bridge scoped to the specific class/selector, not a wildcard.
- Empty states use the shared `EmptyState` component (`src/components/AdConsole/details/EmptyState.tsx`) — icon + title + optional message, not a bare `Card` with a muted paragraph.

### Validation
Engine functions fail fast: invalid input throws `ValidationError` (`src/lib/validation.ts`) rather than silently clamping or producing `NaN`. Follow this pattern for new engine functions — don't add silent fallbacks.
Engine functions fail fast: invalid input throws `ValidationError` (`src/lib/validation.ts`) rather than silently clamping or producing `NaN`. Follow this pattern for new engine functions — don't add silent fallbacks. `MIN_BID` and `assertValidBid` (also in `src/lib/validation.ts`) enforce the $0.02 platform bid floor for "set an explicit bid" actions (`setTargetBid`, `setAdGroupDefaultBid`); creation/normalization paths (`addTarget`, `normalizeCampaign`) still clamp instead of throwing, since those fill in defaults for incomplete data rather than acting on explicit user intent. Relative adjustments (`adjustTargetBid`, the "±10%" buttons) floor at `MIN_BID` rather than fail fast, since the caller doesn't fully control the resulting value.

## Testing conventions

Expand All @@ -117,6 +119,7 @@ These come from `AGENTS.md`, `LOOP.md`, `loop-constraints.md`, and `gate.yaml`
- `legacy/` holds the pre-Next.js prototype (a single-file `amazon_ppc_simulator.html` with inline JS) and its old QA/docs — historical reference only, not part of the current build.
- `codegraphs/Amazon-ad-console.md` describes that old single-file prototype and is stale relative to the current Next.js/engine architecture described above; don't rely on it.
- `docs/` has deeper reference material: `ARCHITECTURE.md`, `API.md` (full engine function signatures), `SCHEMA.md`, `FEATURES.md`, `INTEGRATION.md` (porting guide), `AUTH.md`, `AUDIT-FOLLOWUPS.md`.
- `CHANGELOG.md` (repo root) tracks notable changes per release starting at 3.6.0; bump `version` in `package.json` (and the unused-but-should-stay-in-sync `coreState.version` in `core/slices/core.ts`) together with a new entry when cutting a release.
- `skills/`, `patterns/`, `gate.yaml`, `STATE.md`, `loop-*.md` support an autonomous triage/fix loop tool used against this repo — not part of the app runtime.

## Porting the engine
Expand Down
8 changes: 4 additions & 4 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,13 +405,13 @@ When clicking a tab-mapped item:

| Range | Label | Behavior |
|-------|-------|----------|
| < 768px | Mobile | Sidebar hidden, hamburger toggle + slide-out drawer |
| 768–1100px | Tablet | Sidebar collapses to 200px, hamburger still available |
| > 1100px | Desktop | Full Amazon Console layout |
| < 768px | Mobile | Desktop sidebar hidden; hamburger toggle + slide-out drawer |
| 768–1100px | Tablet | Same as mobile — desktop sidebar hidden, hamburger drawer takes over (both are `isMobileOrTablet` in `useBreakpoint`) |
| > 1100px | Desktop | Full Amazon Console layout with the persistent left sidebar |

### Mobile Drawer
- Hamburger button in the global nav toggles a slide-out drawer
- Drawer contains all sidebar groups: Campaign Manager, Portfolios, Measurement
- Drawer contains all sidebar groups for the active section: Campaign Manager, Portfolios, Measurement, or Training (Drills/Missions/Reports/Bulk ops/Trainer/Integrity)
- Backdrop overlay with click-to-close
- Escape key closes the drawer
- Animation state machine: closed → open ↔ closing → closed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "amazon-ad-console",
"version": "3.5.0",
"version": "3.6.0",
"private": true,
"description": "Amazon PPC Training Simulator — Amazon Ads Console replica for VA training",
"scripts": {
Expand Down
21 changes: 16 additions & 5 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
--danger-soft: #fde7ee;
--info: #007185; /* Amazon clickable teal (same as focus) */
--info-soft: #e0f2f5;
--purple: #7c3aed; /* SD campaign-type badge */
--purple-soft: #f3e8ff;

/* Typography — Premium font stack */
--font-display: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
Expand Down Expand Up @@ -147,8 +149,16 @@
color-scheme: light;
}

/* Reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Reset
* NOTE: padding is intentionally NOT zeroed on `*`. Astryx components ship
* their own padding as StyleX classes inside `@layer astryx-base`/
* `@layer astryx-theme`; per the CSS cascade-layers spec, ANY unlayered
* declaration beats a layered one regardless of specificity. A blanket
* `* { padding: 0 }` here (unlayered) was silently zeroing out every
* Astryx `padding` prop (Card, etc.) sitewide. Reset padding only on the
* couple of native elements that actually need it. */
*, *::before, *::after { box-sizing: border-box; margin: 0; }
ul, ol { padding: 0; }

html {
font-family: var(--font-body);
Expand Down Expand Up @@ -1072,7 +1082,7 @@ code, pre {

.split {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
gap: var(--space-5);
}

Expand Down Expand Up @@ -1243,8 +1253,8 @@ textarea.input {
}

.pill.purple {
background: #f3e8ff;
color: #7c3aed;
background: var(--purple-soft);
color: var(--purple);
}

.pill.green {
Expand Down Expand Up @@ -3136,6 +3146,7 @@ textarea.input {
}

.text-accent { color: var(--landing-accent); }
.object-cover { object-fit: cover; }

/* Navigation */
.landing-nav {
Expand Down
Loading
Loading