store-only apps edit their listing on the store (storebound listing live / set: old -> new per field with the checks, a PATCH of only the edited fields; serve listing.set is the one store-only write, its token bound to the store ids; policy.rs STORE_ONLY_WRITES, write_with_confirm for store targets); screenshot captions per language and scene edited as a text edit of the shots copy JSON (JSON files keep their layout); Play content rating, target audience and App content answers in App info; docs/console-parity.md after the editors (73 rows: 55 complete, 18 with the reason) and tests/console-parity.test.ts keeping it honest

This commit is contained in:
CRTkafa 2026-10-14 00:53:38 +08:00
parent 3cea2af4e1
commit 7ac028421a
33 changed files with 852 additions and 157 deletions

View file

@ -7,7 +7,11 @@
export type EditOp =
| { kind: 'config'; path: string; value?: unknown; remove?: boolean }
| { kind: 'listing'; store: 'play' | 'apple'; lang: string; field: string; value: string }
| { kind: 'contact'; field: 'firstName' | 'lastName' | 'phone' | 'email'; value: string };
| { kind: 'contact'; field: 'firstName' | 'lastName' | 'phone' | 'email'; value: string }
| { kind: 'shotsCopy'; lang: string; scene: string; field: 'eyebrow' | 'title' | 'sub'; line?: 0 | 1; value: string };
/** An edit without its value: what a field edits. */
export type FieldOp = { kind: 'config'; path: string } | Omit<Extract<EditOp, { kind: 'listing' }>, 'value'> | Omit<Extract<EditOp, { kind: 'contact' }>, 'value'> | Omit<Extract<EditOp, { kind: 'shotsCopy' }>, 'value'>;
/** Which editor (tab) a pending edit belongs to, for the unsaved marks. */
export type EditArea = 'listings' | 'appinfo' | 'catalog' | 'testflight' | 'art';
@ -15,7 +19,8 @@ export type EditArea = 'listings' | 'appinfo' | 'catalog' | 'testflight' | 'art'
export interface Pending { key: string; op: EditOp; area: EditArea; /** The saved value, to show "was" and to drop an edit typed back to it. */ saved: unknown }
export const keyOf = (op: EditOp): string =>
op.kind === 'config' ? `config:${op.path}` : op.kind === 'listing' ? `listing:${op.store}:${op.lang}:${op.field}` : `contact:${op.field}`;
op.kind === 'config' ? `config:${op.path}` : op.kind === 'listing' ? `listing:${op.store}:${op.lang}:${op.field}`
: op.kind === 'shotsCopy' ? `shotsCopy:${op.lang}:${op.scene}:${op.field}${op.line === undefined ? '' : `:${op.line}`}` : `contact:${op.field}`;
const valueOf = (op: EditOp): unknown => (op.kind === 'config' ? (op.remove ? undefined : op.value) : op.value);

View file

@ -16,6 +16,7 @@ export const TTL: Record<string, number> = {
'prices.current': 30 * MIN,
'project.listings': 30 * MIN,
'project.editable': 30 * MIN,
'listing.live': 30 * MIN,
'apple.ppo_status': 30 * MIN,
validate: 30 * MIN,
scan: 30 * MIN,
@ -56,11 +57,13 @@ export function readsFor(o: { folder: boolean; ios: boolean; android: boolean })
if (o.android) add('play.status');
if (o.ios || o.android) { add('reviews.list', { days: 30 }); add('health'); }
if (o.folder) {
add('scan'); add('project.listings');
add('scan'); add('project.listings'); add('project.editable');
if (o.ios || o.android) { add('catalog.diff'); add('prices.current'); add('validate'); }
add('analytics.compare'); add('aso.audit');
}
if (o.ios) add('apple.ppo_status');
// store-only apps: the listing editor's live texts
if (!o.folder && (o.ios || o.android)) add('listing.live');
return r;
}