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

@ -4,7 +4,7 @@ import { StarIcon } from '@phosphor-icons/react/dist/csr/Star';
import type { ReactNode } from 'react';
import type { ReleasePhase, StoreRelease } from '../api';
import { t } from '../i18n';
import { share } from '../lib/format';
import { ago, share } from '../lib/format';
import { hueOf, monogram, type Tone } from '../lib/model';
import { fixHint } from '../lib/views';
import { Icon, type IconName, StoreGlyph } from './icons';
@ -173,3 +173,17 @@ export function StudioMark({ studio, size = 'md' }: { studio: { name: string; co
export function StoreOnlyTag() {
return <span className="chip outline so-tag" title={t('apps.storeOnlyHint')}><Icon name="cloud" />{t('apps.storeOnly')}</span>;
}
/** The state of one read beside its card's title: "read 3 min ago" with a small spinner while it refreshes, or
* "couldn't refresh: <reason>" over the last good value, and the per-card refresh icon (no "Read" button first). */
export function ReadControl({ r, label }: { r: { data: unknown; at: string | null; loading: boolean; error: string | null; refresh: () => Promise<void> }; label?: string }) {
const text = r.error ? t('read.couldNotRefresh', { reason: r.error }) : r.at ? t('read.ago', { when: ago(r.at) }) : r.loading ? t('read.waiting') : '';
return (
<span className="read-ctl" data-read-state={r.loading ? 'loading' : r.error ? 'error' : r.data !== null ? 'ok' : 'none'}>
{text && <span className={`read-stamp ${r.error ? 'read-err' : ''}`} title={r.error ?? undefined}>{text}</span>}
<button className="btn btn-sm btn-icon" onClick={() => { r.refresh(); }} disabled={r.loading} aria-label={label ?? t('read.refresh')} title={label ?? t('read.refresh')}>
<Icon name="refresh" className={r.loading ? 'spin' : undefined} />
</button>
</span>
);
}