Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion docs/CODEBASE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
98 changes: 65 additions & 33 deletions docs/WORK_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion mobile/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 13 additions & 5 deletions mobile/src/components/WhatsNewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Easing,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
Expand Down Expand Up @@ -71,7 +72,11 @@ export function WhatsNewCard({ entry, onDismiss }: Props) {
<Text style={styles.title}>What's new</Text>
<Text style={styles.version}>Version {entry.version}</Text>

<View style={styles.list}>
<ScrollView
style={styles.highlights}
contentContainerStyle={styles.list}
accessibilityLabel="Release highlights"
>
{entry.highlights.map((line, i) => (
<View key={i} style={styles.item}>
<Ionicons
Expand All @@ -80,10 +85,10 @@ export function WhatsNewCard({ entry, onDismiss }: Props) {
color={colors.success}
style={styles.itemIcon}
/>
<Text style={styles.itemText}>{line}</Text>
<Text testID="release-highlight" style={styles.itemText}>{line}</Text>
</View>
))}
</View>
</ScrollView>

<Pressable
onPress={onDismiss}
Expand Down Expand Up @@ -112,8 +117,10 @@ const createStyles = (colors: ThemeColors) =>
cardWrap: {
width: '100%',
maxWidth: 420,
maxHeight: '100%',
},
card: {
flexShrink: 1,
backgroundColor: colors.surface,
borderRadius: radius.xl,
borderWidth: 1,
Expand Down Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions mobile/src/data/whatsNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
13 changes: 12 additions & 1 deletion mobile/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
39 changes: 36 additions & 3 deletions mobile/tests/offline.browser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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','');
Expand Down Expand Up @@ -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');
Expand Down