diff --git a/docs/CODEBASE_MAP.md b/docs/CODEBASE_MAP.md
index 32927ea..571e7b1 100644
--- a/docs/CODEBASE_MAP.md
+++ b/docs/CODEBASE_MAP.md
@@ -109,6 +109,8 @@ typography, shadows, and scaling; `ThemeContext` persists light/dark preference.
- `data/whatsNew.ts`, `hooks/useWhatsNew.ts`, and `services/whatsNewStore.ts` control
version announcements. Every release includes a matching one-time card focused
on new features and user-visible improvements, per `RELEASING.md`.
+ `components/WhatsNewCard.tsx` scrolls long highlight lists independently of the
+ heading/dismissal controls so small screens can reach every item.
- `src/types/index.ts` defines shared concept, progress, daily, history, and
streak types. API payloads also have types near their service consumers.
@@ -232,7 +234,7 @@ session pooler. Applied migrations must not be rewritten.
findings as issues. `cleanup.yml` manages stale issues; Dependabot schedules
dependency updates with Expo-managed version restrictions. The checked-in
workflows do not include a general PR pytest job.
-- `mobile/app.config.js` currently has app version `1.7.1` and native runtime
+- `mobile/app.config.js` currently has app version `1.8.0` and native runtime
`1.3.0`; `package.json`'s `1.0.0` is not the release-version authority.
## Documentation drift to remember
diff --git a/docs/WORK_LOG.md b/docs/WORK_LOG.md
index 99237f8..a630a05 100644
--- a/docs/WORK_LOG.md
+++ b/docs/WORK_LOG.md
@@ -7,39 +7,71 @@ claims as completed work.
## Current status
-- Implemented and validated [#182](https://github.com/Coding-Moves/one-concept/issues/182)
- on `codex/182-offline-saved-password` from refreshed `develop` (`58e9b1d`).
- Added show/hide controls for sign-in/signup. Verified the existing full-lesson
- cache across offline restart, and strengthened coverage of saved examples.
- Published [PR #189](https://github.com/Coding-Moves/one-concept/pull/189) into
- `develop`; phone rollout requires the next release.
-- PR #188 is merged. The owner reports installing all three email templates and
- enabling the notification in Supabase; actual inbox delivery remains unverified.
-
-- Published the three branded Supabase templates in non-draft
- [PR #188](https://github.com/Coding-Moves/one-concept/pull/188) into `develop`,
- from `a6e81f1` on `codex/branded-auth-email-templates`. GitHub reports the PR
- mergeable with no conflicts; no CI checks are configured for this PR.
-- Resend delivery preparation remains in [draft PR #187](https://github.com/Coding-Moves/one-concept/pull/187).
- Its duplicate template files are removed in a follow-up commit. Original
- histories are intact. Template installation is independent of a domain purchase
- or app release and still requires pasting HTML into Supabase.
-- No SMTP settings, live templates, notifications, or production release were
- changed. Real inbox rendering and delivery remain unverified.
-
-- Implemented and validated [#151](https://github.com/Coding-Moves/one-concept/issues/151),
- shared daily Gemini generation budget, on `codex/151-shared-generation-budget`
- from refreshed `origin/develop` (`65eb21d`). Published
- [PR #186](https://github.com/Coding-Moves/one-concept/pull/186) into `develop`.
-- Scheduled refill, API prefetch, and manual catalog rewrites now share persisted
- daily reservations. Reruns/restarts retain usage; a denied reservation cannot
- spend a backlog attempt. All 145 backend tests passed with no skips.
-- Migration `0010_generation_daily_usage.sql` was applied only to disposable
- PostgreSQL 16. It must be applied before production deployment; the production
- ledger remains unchanged. The README records consistent settings and first-day
- rollout handling. Mobile and production release work are outside this chunk.
-- PR #185 (#150) is merged. Preserve the owner's identity and the preference for
- more focused commits in this and future PRs.
+- Preparing version **1.8.0** on `codex/1-8-0-release-prep`, from refreshed
+ `develop` (`15aad70`). Feature/fix PRs #183, #184, #185, #186, #188, and #189
+ are merged. Runtime remains **1.3.0** for this JavaScript-only mobile release.
+- The version and six-benefit What's New card are implemented and verified in
+ [preparation PR #190](https://github.com/Coding-Moves/one-concept/pull/190).
+ Merge it into `develop`, then open the single
+ `develop` → `main` release PR after the database prerequisite is satisfied.
+ Production merge/deployment is not part of this request.
+- Production read-only inspection confirmed migration 0010 is missing. Automatic
+ approval review rejected applying it because the release-PR request did not
+ separately authorize a production schema change. No migration was executed and
+ `backend/migrations/applied.txt` is unchanged. Explicit owner authorization is
+ needed for this step before the release PR, as required by `RELEASING.md`.
+- Resend setup remains deferred in [draft PR #187](https://github.com/Coding-Moves/one-concept/pull/187).
+ The owner reports installing all three merged email templates and enabling the
+ password-change notification in Supabase; actual inbox delivery is unverified.
+
+## Version 1.8.0 preparation — 2026-09-13
+
+- **Scope:** wind up merged work and open the release PR with its required
+ version-matched, one-time What's New card. Keep original focused commits and
+ leave production release merging to the owner.
+- **Highlights:** downloaded saved lessons/examples offline; automatic action
+ synchronization on reconnect/reopen; persistent offline topic choices; lighter
+ startup and older Saved search/filter access; password visibility; clearer
+ account emails. The deferred SMTP-provider draft is excluded.
+- **Card:** allow the highlight list to scroll while the heading and Got it stay
+ visible. Keep the existing device/version dismissal key and sign-in gating.
+ The browser regression checks the actual exported version, both themes at
+ 320×568, 390×844, and 844×390, the last highlight, and offline restart after
+ dismissal. No native dependency or runtime change is needed.
+- **Commits:** `d087182` records scope; `e846760` makes long release cards readable
+ with regression coverage; `1e92332` sets version 1.8.0 and its six highlights.
+ `docs: record 1.8.0 release validation and migration prerequisite` records the
+ test instructions, map, and this handoff.
+- **Validation:** all **145 backend tests passed, no skips**, using disposable
+ PostgreSQL 16 and dummy Auth configuration with generation disabled. TypeScript
+ and all **33 Node 24 tests passed, no skips**. Android and web production exports
+ passed with dummy public configuration. The new card browser scenario passed
+ all six viewport/theme combinations and dismissal across offline restart;
+ screenshots were inspected. An initial web export retained the old Expo config
+ version in Metro's cache; rebuilding with `--clear` resolved it. No application
+ code workaround was needed. Artifacts remain under `/tmp/one-concept-1-8-*`.
+- **Limits:** physical-device/native accessibility checks and actual inbox
+ delivery were not exercised. This release reran backend/unit/card checks;
+ #189's documented password and full offline-collection acceptance checks remain
+ the coverage for those unchanged flows.
+- **Database prerequisite:** a read-only query against the configured One Concept
+ production project found no `public.generation_daily_usage` table. Automatic
+ approval review rejected executing `0010_generation_daily_usage.sql` before it
+ ran; neither production nor the applied ledger was changed. The prepared SQL
+ creates a backend-only usage table with its primary key, nonnegative counter
+ constraint, and row-level security. Obtain explicit owner authorization, apply
+ and verify it, then record its filename through a focused PR into `develop`.
+- **Deployment handoff:** the shared generation-budget rollout in
+ [backend/README.md](../backend/README.md#shared-generation-budget) still
+ requires consistent caps across API/workers and pausing old generators during
+ rollout. Enable the new generators at the next Pacific reset, or seed today's
+ usage conservatively while paused. No live worker settings were changed.
+- **PR:** [#190](https://github.com/Coding-Moves/one-concept/pull/190), ready for
+ review into `develop`, preserves the focused preparation commits.
+ `docs: link 1.8.0 preparation PR` records publication; check GitHub for its
+ resulting merge state. The production release PR has not been opened.
+- **Next step:** merge #190, obtain authorization and complete the database
+ prerequisite, then open `develop` → `main` without merging production.
## Saved reading offline and password visibility (#182) — 2026-09-12
diff --git a/mobile/app.config.js b/mobile/app.config.js
index 254e010..ba62a98 100644
--- a/mobile/app.config.js
+++ b/mobile/app.config.js
@@ -18,7 +18,7 @@ module.exports = {
name: 'One Concept',
slug: 'one-concept',
owner: 'coding-moves',
- version: '1.7.1',
+ version: '1.8.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
diff --git a/mobile/src/components/WhatsNewCard.tsx b/mobile/src/components/WhatsNewCard.tsx
index fd12a82..19a804c 100644
--- a/mobile/src/components/WhatsNewCard.tsx
+++ b/mobile/src/components/WhatsNewCard.tsx
@@ -5,6 +5,7 @@ import {
Easing,
Modal,
Pressable,
+ ScrollView,
StyleSheet,
Text,
View,
@@ -71,7 +72,11 @@ export function WhatsNewCard({ entry, onDismiss }: Props) {
What's new
Version {entry.version}
-
+
{entry.highlights.map((line, i) => (
- {line}
+ {line}
))}
-
+
cardWrap: {
width: '100%',
maxWidth: 420,
+ maxHeight: '100%',
},
card: {
+ flexShrink: 1,
backgroundColor: colors.surface,
borderRadius: radius.xl,
borderWidth: 1,
@@ -149,10 +156,11 @@ const createStyles = (colors: ThemeColors) =>
marginTop: 2,
marginBottom: spacing.lg,
},
- list: {
- gap: spacing.md,
+ highlights: {
+ flexShrink: 1,
marginBottom: spacing.lg,
},
+ list: { gap: spacing.md },
item: {
flexDirection: 'row',
alignItems: 'flex-start',
diff --git a/mobile/src/data/whatsNew.ts b/mobile/src/data/whatsNew.ts
index b32e169..4be8f70 100644
--- a/mobile/src/data/whatsNew.ts
+++ b/mobile/src/data/whatsNew.ts
@@ -21,6 +21,17 @@ export interface WhatsNewEntry {
}
export const WHATS_NEW: WhatsNewEntry[] = [
+ {
+ version: '1.8.0',
+ highlights: [
+ 'Read saved lessons and examples offline after they download to your device.',
+ 'Likes, saves, and progress sync automatically when you reconnect or reopen the app.',
+ 'Choose topics offline and keep your selections between visits.',
+ 'Enjoy a lighter startup and find older saved lessons with search and topic filters.',
+ 'Show or hide your password when signing in or creating an account.',
+ 'Refreshed account emails make confirmation, password resets, and security updates clearer.',
+ ],
+ },
{
version: '1.7.1',
highlights: [
diff --git a/mobile/tests/README.md b/mobile/tests/README.md
index 52bae15..57b44fd 100644
--- a/mobile/tests/README.md
+++ b/mobile/tests/README.md
@@ -51,15 +51,19 @@ CI=1 EXPO_NO_DOTENV=1 EXPO_NO_TELEMETRY=1 EXPO_OFFLINE=1 \
EXPO_PUBLIC_API_BASE_URL=http://127.0.0.1:4781/api \
EXPO_PUBLIC_SUPABASE_URL=http://127.0.0.1:4781 \
EXPO_PUBLIC_SUPABASE_ANON_KEY=test-public-key \
- npx expo export --platform web --output-dir /tmp/one-concept-offline
+ npx expo export --platform web --clear --output-dir /tmp/one-concept-offline
node tests/offline.browser.cjs /tmp/one-concept-offline \
--no-event --retry-error --signout-inflight
node tests/offline.browser.cjs /tmp/one-concept-offline --midnight
node tests/offline.browser.cjs /tmp/one-concept-offline --partial-connectivity
node tests/offline.browser.cjs /tmp/one-concept-offline --large-collections --require-compact
+node tests/offline.browser.cjs /tmp/one-concept-offline --whats-new
node tests/password.browser.cjs /tmp/one-concept-offline
```
+`--clear` ensures a changed app version reaches the export instead of reusing
+stale Expo configuration from Metro.
+
The flags exercise timer-only reconnection, a 503 during replay, and a request
that fails after sign-out. Omit `--no-event` to test the browser online event.
The separate `--midnight` scenario holds a like request across the date change,
@@ -79,6 +83,13 @@ For the unchanged pre-fix export, `--baseline` asserts the original #133 failure
The test uses port 4781 and closes its server/browser afterward. Optional
`OFFLINE_SCREENSHOT_PATH` saves the offline detail view for visual inspection.
+`--whats-new` checks the current version's card in light/dark themes at small
+phone, standard phone, and landscape sizes. The title and Got it must stay fully
+visible, the final highlight must scroll into view, and dismissal must persist
+through an offline restart. Set `WHATS_NEW_SCREENSHOT_DIR` to save all six previews.
+Use a version with a nonempty What's New entry; the test intentionally fails if
+that required release content is missing or the export has a stale version.
+
`password.browser.cjs` uses the same export, Playwright settings, and port 4781;
run the browser scripts sequentially. It starts signed out and intercepts
authentication with dummy responses. It checks visibility and keyboard control
diff --git a/mobile/tests/offline.browser.cjs b/mobile/tests/offline.browser.cjs
index 6838f6e..c59c799 100644
--- a/mobile/tests/offline.browser.cjs
+++ b/mobile/tests/offline.browser.cjs
@@ -9,6 +9,7 @@ if (!root || !fs.existsSync(path.join(root, 'index.html'))) throw new Error('Pas
const baseline = process.argv.includes('--baseline');
const midnight = process.argv.includes('--midnight');
const largeCollections = process.argv.includes('--large-collections');
+const whatsNew = process.argv.includes('--whats-new');
const date = new Date().toISOString().slice(0, 10);
const concept = (slug, title) => ({ id: slug, slug, title, summary: `Full explanation of ${title}.`, example: `A concrete example of ${title}.`, topic_slug: 'computer-science', topic_name: 'Computer Science', like_count: 2 });
const daily = concept('fixture-daily', 'Daily fixture');
@@ -45,14 +46,14 @@ const server = http.createServer((req,res) => {
const browser = await chromium.launch({ executablePath:process.env.PLAYWRIGHT_CHROMIUM_PATH, headless:true, args:['--no-sandbox'] });
try {
const context = await browser.newContext({viewport:{width:390,height:844},timezoneId:'UTC'});
- await context.addInitScript(({session, appVersion}) => {
+ await context.addInitScript(({session, appVersion, whatsNew}) => {
Object.defineProperty(navigator, 'share', {configurable:true,value:async data=>{window.fixtureShared=data;}});
if (!localStorage.getItem('fixture-seeded')) {
localStorage.setItem('sb-127-auth-token', JSON.stringify(session));
- localStorage.setItem('one-concept/last-seen-version/v1',appVersion);
+ localStorage.setItem('one-concept/last-seen-version/v1',whatsNew ? 'previous-version' : appVersion);
localStorage.setItem('fixture-seeded','yes');
}
- }, {session, appVersion});
+ }, {session, appVersion, whatsNew});
await context.route('**/api/**', async route => {
if (!online) return route.abort('internetdisconnected');
const req = route.request(); const endpoint = new URL(req.url()).pathname.replace('/api','');
@@ -107,6 +108,38 @@ const server = http.createServer((req,res) => {
const queue=async()=>page.evaluate(()=>JSON.parse(localStorage.getItem('one-concept/mutation-queue/v1')||'{}'));
if (midnight) await page.clock.setFixedTime(new Date(date+'T23:59:00Z'));
await page.goto('http://127.0.0.1:4781');
+ if (whatsNew) {
+ const heading = page.getByText("What's new", {exact:true});
+ const gotIt = page.getByRole('button', {name:'Got it', exact:true});
+ await expect(heading).toBeVisible();
+ await expect(page.getByText('Version ' + appVersion, {exact:true})).toBeVisible();
+ for (const colorScheme of ['light', 'dark']) {
+ await page.emulateMedia({colorScheme});
+ for (const viewport of [{width:320,height:568}, {width:390,height:844}, {width:844,height:390}]) {
+ await page.setViewportSize(viewport);
+ await expect(heading).toBeInViewport({ratio:1});
+ await expect(gotIt).toBeInViewport({ratio:1});
+ const last = page.getByTestId('release-highlight').last();
+ await last.scrollIntoViewIfNeeded();
+ await expect(last).toBeInViewport({ratio:1});
+ await expect(gotIt).toBeInViewport({ratio:1});
+ if (process.env.WHATS_NEW_SCREENSHOT_DIR) {
+ fs.mkdirSync(process.env.WHATS_NEW_SCREENSHOT_DIR, {recursive:true});
+ await page.screenshot({path:path.join(process.env.WHATS_NEW_SCREENSHOT_DIR, `${colorScheme}-${viewport.width}x${viewport.height}.png`)});
+ }
+ }
+ }
+ await gotIt.click();
+ await expect(heading).toHaveCount(0);
+ await expect.poll(() => page.evaluate(() => localStorage.getItem('one-concept/last-seen-version/v1'))).toBe(appVersion);
+ online=false;
+ await page.reload();
+ await expect(page.getByText(daily.summary, {exact:true})).toBeVisible();
+ await expect(heading).toHaveCount(0);
+ assert.deepEqual(errors, []);
+ console.log('PASS: release card fits phone/landscape layouts in both themes, all highlights scroll into view, and dismissal survives offline restart');
+ return;
+ }
await expect(page.getByText(daily.summary,{exact:true})).toBeVisible();
if (largeCollections) {
assert.equal(savedRequests,0,'Older Saved metadata must not load on startup');