Brand and design · Specs
Data binding — every visible data point, per screen
Every visible data point to its API field or local state, and the contract change requests.
Every number, label and piece of content in the hi-fi screens is traceable to an API field in docs/api/openapi.yaml, a documented local state, or a derivation from those two. In the HTML, each bound element has a data-bind="…" attribute. Press b on any screen (or use "Show data-bind" in its spec panel or the gallery) to outline them. screens/manifest.json lists every bind per screen file. Example JSON for every response lives in docs/api/examples/en/ and docs/api/examples/zh-Hans/, indexed by index.json.
Notation
Prefix in data-bind |
Meaning |
|---|---|
me.* |
GET /v1/me → Me |
home.* |
GET /v1/home → Home |
session.* |
GET /v1/sessions/{id} / POST /v1/sessions → Session (session.article = Article) |
attempt.* |
GET /v1/attempts/{id} → Attempt |
assessment.* |
Attempt.assessment (round 1 feedback; diagnostic result) |
exemplar.* |
GET /v1/attempts/{id}/exemplar → Exemplar |
settlement.* |
POST /v1/sessions/{id}/complete → Settlement |
articles.items[n] |
GET /v1/articles → ArticleSummary |
growth.* |
GET /v1/growth → Growth |
weekly_quests.* |
GET /v1/weekly-quests → WeeklyQuests |
plan.* |
GET /v1/plan → Plan |
paywall.* |
GET /v1/paywall → Paywall |
config.* |
GET /v1/config → Config |
storekit[...] |
StoreKit 2 Product for that id (never the API) |
onboarding.* |
in-memory OnboardingAnswers until POST /v1/onboarding |
local:* |
device state (recorder, player, cache, network, filters) |
derived: |
computed on device from the fields named |
(Accept-Language) |
a UI-locale string the API localizes from the request's Accept-Language header (ADR-001 D13). The client sends its UI locale (settings.ui_locale ?? device) on every request and re-fetches when it changes. |
(article language) |
content in session.language / article.language, regardless of UI locale. Render with lang= (HTML) / .environment(\.locale, Locale(identifier: language)) (SwiftUI) so VoiceOver uses the right voice. |
Typed sources. botgumi's check needs one typed source per shown value (api GET /home level.title, local recorder.state, device network; botgumi ADR-0006 D2), resolved against openapi.yaml. tools/typed_sources.py reads them from this notation when it exports screens.json: a.b / c is a.b and a.c, [n] is every item, a bare name in a row continues the reference before it. Where a row is prose, that file lists its sources by hand, and it declares every local, device, store and derived name once. The iOS APIs in this page map to platform-neutral names there (NWPathMonitor is device network, Product.purchase() is store purchase).
General fallbacks (unless a row says otherwise):
- Loading: skeleton blocks of the same size. Never "0" or "—" for a number that is still loading.
- Null optional number: hide the element (or show "—" in a table cell), never 0.
- Null server string: use the client fallback key in
copy.md. - Offline: last cached value + offline banner with the cache time (
local:cache.<endpoint>.fetchedAt).
Contract change requests
The design track asks; the backend track edits openapi.yaml. CCR-1 to CCR-13 were applied in #8 (6810253): a CCR-n mark in the tables below now names where that field came from, and the fallback no longer applies. An open request is still missing from the contract: the fields that need it are pending in screens.json and fall back as noted.
| # | Request | Why (screen) | Fallback until merged | Status |
|---|---|---|---|---|
| CCR-1 | Home.today_quest → nullable: true (no eligible article in the user's practice languages / topics today). |
home--no-quest: today the schema forces a quest object, so the server would have to invent one. |
Server must always return a quest; the no-quest state is unreachable. | applied (#8) |
| CCR-2 | Home.today_quest.summary: SessionSummary | null once status = completed. |
home--completed shows "68 → 79 · 41 s shorter · +55 XP". |
Hide the result row; show only the "Done" tag and the free-practice CTA. | applied (#8) |
| CCR-3 | ArticleSummary.unlock_hint: string | null (Accept-Language), e.g. "Unlocks at ability 80". |
Locked rows in the library. The client must not hard-code tier rules (D9 can change). | Generic library.row.locked fallback copy. |
applied (#8) |
| CCR-4 | Clarify today_quest.xp_reward = base XP excluding the improvement bonus, and add xp_reward_max (base + improve). |
Quest badge shows "+45 XP". A user who improves earns 55; showing max would over-promise and showing base under-promises. | Show xp_reward as is (base). |
applied (#8) |
| CCR-5 | Growth.min_sessions_per_point (proposed 2) and series[].is_partial (current week, in progress). |
The growth chart's dashed "next point" marker and the "train tomorrow to unlock this week's point" hook. Today a week with one session would draw a point from n=1. | Treat value = null && sample_n > 0 as partial. |
applied (#8) |
| CCR-6 | ErrorEnvelope.error.request_id: string. |
Generic error and processing-failed screens show "request 7f3a-91c2" for support. | Omit the request id. | applied (#8) |
| CCR-7 | Expose answers: Me.onboarding.answers: OnboardingAnswers and Paywall.default_weekly_days. |
Plan card "5 days a week", paywall slider default, Settings. After reinstall / SIWA on a new device, local answers are gone. | Use the locally cached answers; default K = 5. | applied (#8) |
| CCR-8 | Streak.last_freeze_used_local_date: date | null. |
Home notice "A streak freeze was used yesterday" (home.freeze_used) after a missed day. Without it a silent freeze looks like a bug. |
No notice. | applied (#8) |
| CCR-9 | Settlement.round1_duration_ms, round2_duration_ms. |
The complete screen's before/after durations (02:18 → 01:37) need a second request today (GET /v1/sessions/{id}), and replayed settlements are often shown offline. |
Second request; hide durations if it fails. | applied (#8) |
| CCR-10 | Enum for Settlement.xp.breakdown[].type: round1 | round2 | complete | improve | weekly_quest, plus weekly_quest_id when type = weekly_quest. ADR-001 must also state whether weekly-quest XP counts toward the 165 daily cap. |
XP breakdown lines are localized client-side by type (complete.xp.<type>). Weekly quest rewards need a line too. |
Unknown types render with the server-sent type string. |
applied (#8) |
| CCR-11 | Paywall.baseline: { overall, source: diagnostic | ability }. |
Expired-return paywall projects from current ability, not the diagnostic. The server should decide, so the projection matches the cohort job. | Client uses me.stats.ability when entitlement.status = expired, else diagnostic.overall. |
applied (#8) |
| CCR-12 | Paywall.diagnostic.band + band_label (Accept-Language). |
Paywall hero "Good foundation". | Client derives the band from D3 thresholds and uses band.* keys. |
applied (#8) |
| CCR-13 | Article.length of the transcript: Attempt.transcript.length exists. Also add Attempt.validity_floor: TextLength. |
The unreliable screen says "17 words (at least 30 needed)". The floor is per language (D2) and must not be hard-coded on the client. | Client table {zh-Hans: 40 chars, en: 30 words} mirrored from D2 (documented duplication). |
applied (#8) |
| CCR-14 | Document the error responses on every operation the screens read: 4XX and 5XX → #/components/responses/Error (ErrorEnvelope), and 402 entitlement_required on the hard-paywalled reads the API description lists (/home, /articles*, /topics, /sessions, /growth, /achievements, /weekly-quests, /plan). Today only a few operations list any error, GET /home none. |
Home's error state and the generic error sheet show error.code, error.message and error.request_id; botgumi's check cannot find an error GET /home documents. |
The client decodes any non-2xx body as ErrorEnvelope, as the API description says. |
open |
Onboarding
| Screen · element | Source | Format | Fallback |
|---|---|---|---|
| welcome · legal links | paywall.legal.terms_url / privacy_url (or config once fetched) |
link | Bundled URLs |
| language · options | config.supported_practice_languages |
endonym (lang.<tag>), device locale first and pre-selected |
[en, zh-Hans] |
| language · selection | onboarding.practice_language |
— | device locale if supported, else en |
| language · UI note | local:Locale.current |
onboarding.language.ui_note |
— |
| goal / scenario / self-level · options | static catalog keys onboarding.<step>.<enum> mapping to the OnboardingAnswers enums |
— | — |
| scenario · topic chips | GET /v1/topics → topics[].name (Accept-Language) |
chip, multi-select → onboarding.preferred_topics |
hide the section if the request fails |
| commitment · 3/5/7 | onboarding.weekly_days |
— | 5 pre-highlighted |
| commitment · wheel | onboarding.reminder_time |
HH:mm local, 5-min steps |
08:30 |
| notification-priming · preview | onboarding.reminder_time, diagnostic article.title |
notif.daily.body |
generic body without title |
| notification-priming · submit | POST /v1/onboarding with all answers; PUT /v1/me/push-token if granted |
— | on failure: inline retry, answers kept |
Diagnostic
| Element | Source | Format | Fallback |
|---|---|---|---|
| intro · language tag | onboarding.practice_language → session.language |
diag.intro.lang with endonym |
— |
| read · title / byline / paragraphs / hint | session.article.* (article language) |
as is; lang= set |
skeleton; on 404 → generic error |
| read · length | session.article.length.label (Accept-Language) |
as is ("307 字" / "152 words") | hide |
| read · read time | session.article.est_read_seconds |
common.read_* (30 s steps) |
hide |
| record · max | config.limits.diagnostic_max_ms |
mm:ss |
01:30 |
| record · timer, waveform, states | local:recorder.* |
mm:ss effective time |
— |
| processing · stages | attempt.progress.stage / stage_index / stage_count, attempt.status |
3 rows | upload stage is client-side (URLSession progress) |
| processing · ETA | attempt.progress.eta_ms |
processing.eta rounded to 5 s |
hide line |
| result · score ring + count-up | assessment.overall, assessment.band (colour), assessment.band_label (Accept-Language) |
0→N count-up 1200 ms | — (screen only shown when assessed) |
| result · summary, notes, evidence, one-action | assessment.* (article language) |
— | dimension with insufficient_evidence → dimension.insufficient |
| result · evidence chips | assessment.evidence[].start_ms |
mm:ss |
— |
| result · key points | assessment.key_point_coverage[] |
covered ✓ / missed ! | hide section if empty |
Plan
| Element | Source | Format | Fallback |
|---|---|---|---|
| focus title | plan.target_micro_skill → micro-skill title (API, Accept-Language; fallback micro.*) |
— | — |
| baseline | plan.baseline_overall |
integer | — |
| reason | the weakest dimension of the diagnostic (derived: lowest assessment.dimensions.<key>.score), its label (dimension.<key>) and assessment.dimensions.<weakest>.note |
plan.reason |
omit sentence |
| tags | weekly days (CCR-7), me.settings.reminder_time, session.language |
— | hide tag |
| day rows | plan.days[].day / date / title / status |
date via Date.FormatStyle (10月17日 / Oct 17) |
GET /v1/plan 404 → plan not ready: spinner + retry |
Paywall
| Element | Source | Format | Fallback |
|---|---|---|---|
| score | paywall.diagnostic.overall |
display | — |
| band label | CCR-12 / derived from D3 | band.* |
— |
| weakest | paywall.diagnostic.weakest_label (Accept-Language) |
paywall.weakest |
hide line |
| title skill | paywall.diagnostic.one_action_title (Accept-Language) |
paywall.title |
generic title without skill |
| K slider | local:paywall.selectedWeeklyDays, default onboarding.weekly_days (CCR-7) |
3 / 5 / 7 detents | 5 |
| projection | derived: paywall.benchmarks[] where weekly_days = K and baseline_bucket = bucket(diagnostic.overall) → day14_avg_overall, sample_n, weakest_dim_avg_delta |
paywall.projection (+ weakest_delta) — see paywall.md |
sample_n < 30 or missing → paywall.projection.insufficient (no number) |
| pilot label | paywall.benchmark_source = pilot_benchmark → paywall.benchmark_label (Accept-Language) |
amber tag | paywall.source.pilot key |
| calendar days | paywall.plan_preview.days[] + derived: practice pattern for K + streak milestones reachable by that pattern + billing day = trial_days |
7-col grid, Monday first | if plan missing: hide calendar, keep timeline |
| timeline dates | derived: today + trial_days − 1 (reminder), today + trial_days (billing), plan day 14 |
Date.FormatStyle |
— |
| billing price | storekit[id].displayPrice + subscriptionPeriod |
paywall.timeline.billing.* |
paywall.timeline.billing.loading |
| product cards | paywall.products[] (kind, trial_days, is_default, badge — badge Accept-Language) × storekit[id] (displayPrice, price, introductoryOffer, isEligibleForIntroOffer) |
per-month = price / 12 with priceFormatStyle |
StoreKit not loaded → skeleton cards, CTA disabled |
| CTA + caption | derived: selected product, trial eligibility, billing date |
paywall.cta.trial / paywall.cta.subscribe + caption |
loading / purchasing / failed / restored states own the CTA |
| expired · stats | me.stats.completed_sessions, me.stats.ability, me.focus.title |
— | ability null → "—" |
| expired · trial end date | me.entitlement.expires_at / trial_ends_at |
date | omit date |
| legal | paywall.legal.terms_url / privacy_url |
link | bundled URLs |
Home
| Element | Source | Format | Fallback |
|---|---|---|---|
| streak pill | home.streak.current / today_done / at_risk |
amber; filled flame when today_done; outline when at_risk; grey at 0 |
cached |
| date label | home.local_date_label (Accept-Language) |
as is | weekday from device |
| greeting + name | home.greeting (Accept-Language) + home.display_name |
home.greeting.format |
name fallback "练习者 / Learner" from API |
| level | home.level.level / title (Accept-Language) / xp_into_level / xp_for_next |
lime-on-ink bar | — |
| quest badge | home.today_quest.xp_reward (CCR-4) |
+N XP |
hide badge |
| quest meta | today_quest.article.topic.name (Accept-Language), article.length.label (Accept-Language), est_read_seconds |
topic · length · read time |
drop missing parts |
| quest title | today_quest.article.title (article language) |
lang= |
— |
| plan day tag | today_quest.plan_day |
home.quest.plan_day |
hide when null |
| focus chip | home.focus.title (Accept-Language) |
home.quest.focus.last (or .diagnostic when focus.suggested_count = 1 and no completed sessions) |
hide when focus null |
| 3-step path | today_quest.steps[].key / label (Accept-Language) / state |
done = lime ✓, current = white ring | — |
| CTA | today_quest.status, session_id |
start / continue (home.quest.cta.resume.<stage>) |
— |
| completed summary | CCR-2 today_quest.summary |
68 → 79, secs_delta, +XP |
hide row |
| streak-risk card | home.streak.at_risk, freezes |
home.risk.* |
hide card |
| topic chips | home.topics[].name (Accept-Language) |
— | hide row |
| quick pick | home.quick_picks[topic = selected] → title, subtitle (Accept-Language), article, topic.glyph/color_token |
— | none for topic → empty card (home.picks.empty.*) |
| resume sheet | home.resume_session.article.title / stage / created_at / round1_overall |
resume.stage |
not shown when null |
| offline banner | NWPathMonitor + local:cache.home.fetchedAt |
home.offline (HH:mm) |
— |
Library
| Element | Source | Format | Fallback |
|---|---|---|---|
| language filter | local:library.filter.language → GET /v1/articles?language=; options = config.supported_practice_languages |
"All languages" + endonyms; default = all practice languages (no param) | — |
| topic chips + counts | GET /v1/topics → name (Accept-Language), article_count |
chip with count | hide counts if missing |
| status segment | local:library.filter.status → ?status=all|unseen|completed |
— | — |
| row tile | articles.items[n].topic.glyph / color_token (glyph Accept-Language: 产 / Pr) |
48pt square | — |
| row title | articles.items[n].title (article language) |
lang= |
— |
| row language tag | articles.items[n].language |
lang.short.<tag> — only when the list mixes languages |
hidden |
| row meta | topic.name, length.label (Accept-Language), difficulty |
topic · length · Level n |
— |
| status tag | user_status, best_overall, locked + CCR-3 unlock_hint |
New / In progress / Best N / lock | — |
| pagination | next_cursor |
infinite scroll | — |
| empty | items = [] for filter |
library.empty.* |
— |
Read
| Element | Source | Format | Fallback |
|---|---|---|---|
| kind tag | session.kind |
read.tag.<kind> |
— |
| topic tag | session.article.topic.name (Accept-Language) |
— | hide |
| language tag | session.language |
lang.short.<tag> (title = endonym) |
— |
| read time | session.article.est_read_seconds |
common.read_* |
hide |
| length | session.article.length.label |
as is | hide |
| title / byline / paragraphs / hint | session.article.* (article language) |
t-reading, lang= |
load error → read.load_error, never another article |
| source line | session.article.source.name, version |
read.source |
hide |
| CTA | POST /v1/sessions/{id}/reading-complete {read_duration_ms = now − local:reader.openedAt} |
— | offline: queued, proceed |
Record (round 1, round 2)
| Element | Source | Format | Fallback |
|---|---|---|---|
| step | session.stage → 2/3 or 3/3 |
— | — |
| focus chip (R1) | session.previous_focus.title (Accept-Language) |
record.focus.round1 |
hide when null |
| previous advice card (R2) | session.round1_assessment.one_action.instruction / example_sentence (article language) |
ink card | — |
| last duration (R2) | session.attempts[round=1, status=assessed].duration_ms |
mm:ss |
hide clause |
| timer | local:recorder.effectiveMs |
mm:ss, tabular |
— |
| max | session.max_round1_ms / max_round2_ms |
/ mm:ss |
config limits |
| min | session.min_valid_ms |
gates CTA; "N more seconds" | 20 000 |
| waveform | local:recorder.levelDb (30 Hz) |
34 bars | flat bars |
| indicator | local:recorder.state |
recording / paused | — |
| interrupted sheet | AVAudioSession.interruptionNotification / route change + local:recorder.fileURL |
record.interrupted.* |
— |
| permission | AVAudioApplication.recordPermission |
denied card + Settings deep link | — |
| submit | POST /v1/sessions/{id}/attempts {round, duration_ms, content_type, size_bytes, sha256, pause_count} → PUT → POST /v1/attempts/{id}/submit |
— | offline: persist + queue (processing--offline) |
Processing
| Element | Source | Format | Fallback |
|---|---|---|---|
| stage rows | attempt.status, attempt.progress.stage / stage_index / stage_count; upload stage = local:uploadQueue |
✓ / spinner / pending | — |
| upload sub | local:recorder.effectiveMs |
processing.sub.upload |
— |
| transcribed length | attempt.transcript.length.label |
processing.done.transcribe |
"Transcribed" without count |
| ETA | attempt.progress.eta_ms |
seconds | hide |
| long wait | config.limits.poll_timeout_ms exceeded |
processing.slow.* |
— |
| failed | attempt.error.code / message (Accept-Language) / retryable / stage |
retry if retryable |
non-retryable → generic error |
| unreliable | attempt.unreliable_reason → unreliable.<reason>.*; attempt.transcript.length vs floor (CCR-13); language_mismatch uses session.language endonym |
— | generic unreliable copy |
| own-audio playback | GET /v1/attempts/{id}/audio |
player | 410 → hide player |
Feedback
| Element | Source | Format | Fallback |
|---|---|---|---|
| "You did it ✓" | assessment.achieved_previous_action = true + previous_action → micro-skill title |
success banner, first element | — |
| "not yet" line | achieved_previous_action = false |
neutral row | hidden when null |
| low-confidence | assessment.confidence = low + confidence_note (article language) |
warning banner | generic sentence |
| score ring | assessment.overall, band (colour), band_label (Accept-Language) |
104pt ring | — |
| summary | assessment.summary (article language) |
title3 | — |
| dimension name | dimension.<key> client key (or API label) |
— | — |
| dimension note / score / evidence chip | assessment.dimensions.<key>.note / score / evidence_ids[0] → evidence.start_ms |
mm:ss chip |
insufficient_evidence → dimension.insufficient |
| one action | assessment.one_action.title / instruction / example_sentence / evidence_id (article language) |
ink card | — |
| evidence list | assessment.evidence[] sorted by start_ms |
quote + note | — |
| playback | GET /v1/attempts/{id}/audio → url (15-min TTL), local:player.positionMs |
seek start_ms, stop end_ms |
410 → feedback.evidence.expired |
| key points | assessment.key_point_coverage[] (article language) |
count derived: |
hide |
| exemplar | exemplar.status / text / duration_ms / label (label Accept-Language; text + voice in article language) |
pending skeleton / ready player / failed text + retry | unavailable → hide section |
| meta | assessment.rubric_version, model_ref, confidence |
caption | — |
| helpful | POST /v1/assessments/{id}/feedback {helpful, reason?} |
thumbs; 👎 opens reason sheet | queued offline |
Complete
| Element | Source | Format | Fallback |
|---|---|---|---|
| eyebrow | session.kind, settlement.replayed |
— | — |
| headline / subline | settlement.headline / subline (Accept-Language) |
— | complete.headline.<verdict> |
| icon + rays | settlement.comparison.verdict |
trophy only for improved |
— |
| before/after | settlement.round1_overall / round2_overall, durations (CCR-9) |
count-up r1→r2 | — |
| session delta | settlement.comparison.overall_delta, duration_delta_ms |
±N, secs_delta |
— |
| lost points | comparison.key_points_lost[] (article language) |
warning banner | hidden when empty |
| XP tile | settlement.xp.awarded |
count-up +N |
— |
| streak tile | settlement.streak.current, streak_incremented |
+1 only when incremented and not replayed |
— |
| ability tile | ability_after − ability_before; ability_sample_count < 3 → "building baseline n/3" |
separate from session delta | — |
| level card | level_after.level / title (Accept-Language) / xp_into_level / xp_for_next; animation from level_before |
bar fill 900 ms | — |
| XP lines | xp.breakdown[] (CCR-10), missing improve shown struck through |
— | — |
| cap / repeat notes | xp.daily_cap_hit, xp.repeat_discount |
complete.xp.cap_hit / .repeat |
— |
| weekly quests | settlement.weekly_quests.quests[] (titles Accept-Language) |
bars | — |
| next focus | settlement.next_suggestion.title / instruction (Accept-Language) |
— | — |
| level-up overlay | level_after.level > level_before.level |
overlay | — |
| achievement sheet | achievements_unlocked[] (Accept-Language), plan_day_completed |
sheet per item, queued | — |
| replayed banner | settlement.replayed, settled_at |
date + time | — |
Growth
| Element | Source | Format | Fallback |
|---|---|---|---|
| range | GET /v1/growth?range=4w|8w|12w |
segmented | 8w |
| language chips | growth.languages[] → ?language= |
shown only when languages.length > 1; "All languages" + endonym + session count |
hidden |
| ability | growth.ability, ability_delta_30d, sample_count, algo_version |
display + delta | null → baseline card growth.baseline.title with sample_count |
| level badge | me.level.level |
— | — |
| trend label | growth.trend |
growth.trend.<value> |
— |
| chart | growth.series[].week_start / value / sample_n |
one point per week; value = null && sample_n = 0 → gap marker "None"; no interpolation; weeks before the first session are blank; partial current week (CCR-5) dashed |
— |
| hook | growth.next_unlock (Accept-Language) |
lock pill | hidden when null |
| dimensions | growth.dimensions[].label (Accept-Language) / score / delta_30d / color_token |
bars | null score → "—" + "shown after 3 sessions" |
| micro-skills | growth.micro_skills[].title (Accept-Language) / suggested / achieved / mastery + current focus = me.focus.micro_skill_id |
pips + % | suggested = 0 → "not suggested yet" |
| achievements | growth.achievements[] (Accept-Language) |
unlocked lime / locked progress | — |
| recent sessions | growth.recent_sessions[] → article.title (article language), completed_at, round1_overall, round2_overall, duration_delta_ms, xp_awarded |
row | hide section when empty |
| empty | growth.state = empty |
empty state | — |
Profile & settings
| Element | Source | Format | Fallback |
|---|---|---|---|
| avatar initial | derived: first grapheme of me.display_name |
violet tile | "?" |
| name / title / level | me.display_name, me.level.title (Accept-Language) / level |
— | — |
| stats | me.streak.current, me.stats.completed_sessions, me.stats.ability |
— | ability null → profile.stat.ability.baseline |
| SIWA card | me.auth_provider = anonymous |
— | hidden for apple |
| weekly quests | weekly_quests.week_start / week_end / quests[] (titles Accept-Language) |
date range | — |
| subscription row | me.entitlement.status / product_id / trial_ends_at / expires_at |
profile.sub.* |
— |
| languages row | me.settings.practice_languages |
endonyms joined | — |
| app language | me.settings.ui_locale (null = follow device); options config.supported_ui_locales |
"Follow system · English" | follow device |
| practice languages | me.settings.practice_languages (first = primary); options config.supported_practice_languages |
toggles; last one can't be turned off | — |
| reminders | me.settings.reminders_enabled / reminder_time / streak_risk_alert + UNUserNotificationCenter status |
toggles | system-off → settings.notifications_off |
| retention | me.settings.audio_retention_days |
7 / 30 / 365 segmented | 30 |
| delete data | DELETE /v1/me/training-data → {job_id, purge_backups_by}; count from me.stats.completed_sessions |
alert | — |
| delete account | DELETE /v1/me → then POST /v1/auth/anonymous (fresh) |
alert | — |
| version | Bundle version/build + config.rubric_version |
caption | — |
Global
| Element | Source | Format | Fallback |
|---|---|---|---|
| resume sheet | home.resume_session (SessionSummary) or local unsent recording |
— | — |
| leave recording | local:recorder.effectiveMs |
— | — |
| offline toast | NWPathMonitor.status |
— | — |
| generic error | error.code, error.message (Accept-Language), CCR-6 request_id |
— | — |
| force update | config.min_app_version vs Bundle.shortVersion |
— | — |