desktop reads everything by itself: one background read scheduler (4 per store, starts spaced per account, App Store Connect X-Rate-Limit slows it below 20 % and pauses at 100 left, backoff with jitter on 429 / RESOURCE_EXHAUSTED, visible app first, one read per key), per-method TTLs over the saved reads, Settings auto-read (on) and interval (15 min, paused while hidden), Refresh all, a refresh icon and 'read 3 min ago' / 'couldn't refresh' per card, writes invalidate, status line with progress and quota; quota reaches the window through ToolResult.quota (cli/lib/quota.ts); Health top issues open with versions, sample stack (health --issue / --signature, read health_issue), Open in Play Console, Copy for AI and likely-cause hints (cli/lib/crash-hints.ts); tests/scheduler.test.ts, tests/quota-crash-hints.test.ts, boot check auto-read case

This commit is contained in:
CRTkafa 2026-10-13 23:36:30 +08:00
parent 89256f1e80
commit 0fb4ce0001
46 changed files with 1531 additions and 141 deletions

View file

@ -15,7 +15,7 @@ import type { UnifiedApp } from '../../lib/identity';
import { useData, useRead } from '../../store';
import { ConsolePlanButton } from '../../ui/ConsolePlan';
import { Icon, StoreGlyph } from '../../ui/icons';
import { Card, Chip, Empty, ErrorNotice, Notice, Seg, Skeleton } from '../../ui/kit';
import { Card, Chip, Empty, ErrorNotice, Notice, ReadControl, Seg, Skeleton } from '../../ui/kit';
import { OwnerAction } from '../../ui/OwnerAction';
import { ascUrl, PLAY_CONSOLE } from '../AppView';
@ -40,9 +40,9 @@ export function AppleSubmitCard({ app, project }: { app: UnifiedApp; project: Pr
const args = { build, ...(version ? { version: version.version } : {}), ...(resolveIaps ? { resolve_iaps: true } : {}) };
const refresh = () => { r.refresh(); };
return (
<Card title={t('sub.appleTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="ios" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : t('sub.appleSub')}</span>}
<Card title={t('sub.appleTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="ios" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.appleSub')}</span>}
actions={<>
<button className="btn btn-sm" onClick={refresh} disabled={r.loading && !!r.data}><Icon name="refresh" className={r.loading && r.data ? 'spin' : undefined} />{r.data ? t('action.refresh') : t('sub.read')}</button>
<ReadControl r={{ ...r, refresh: async () => refresh() }} label={t('sub.read')} />
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(ascUrl(app.ios?.app.id ?? summary?.apple?.appId)).catch(() => {})}><Icon name="external" />App Store Connect</button>
</>}>
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}
@ -142,9 +142,9 @@ export function PlayProductionCard({ app, project }: { app: UnifiedApp; project:
const picked = s?.codes.find(c => c.code === code) ?? null;
const args = { version_code: code, status, ...(status === 'inProgress' ? { percent: Number(share) } : {}) };
return (
<Card title={t('sub.playTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="android" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : t('sub.playSub')}</span>}
<Card title={t('sub.playTitle')} icon="rocket" sub={<span className="row" style={{ gap: 6 }}><StoreGlyph store="android" className="ic13" />{r.data ? (r.fromCache ? t('common.cachedFrom', { when: ago(r.at) }) : t('common.readWhen', { when: ago(r.at) })) : r.loading ? t('read.waiting') : t('sub.playSub')}</span>}
actions={<>
<button className="btn btn-sm" onClick={() => { r.refresh(); }} disabled={r.loading && !!r.data}><Icon name="refresh" className={r.loading && r.data ? 'spin' : undefined} />{r.data ? t('action.refresh') : t('sub.read')}</button>
<ReadControl r={r} label={t('sub.read')} />
<button className="btn btn-sm btn-ghost" onClick={() => api.openExternal(PLAY_CONSOLE).catch(() => {})}><Icon name="external" />Play Console</button>
</>}>
{r.error && <ErrorNotice error={r.error} keep={!!r.data} />}