@rightkit/social-ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE-APACHE +201 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +40 -2
  5. package/dist/appName.d.ts +2 -0
  6. package/dist/appName.js +8 -0
  7. package/dist/components/ScopeSelect.d.ts +7 -0
  8. package/dist/components/ScopeSelect.js +6 -0
  9. package/dist/components/Shared.d.ts +34 -0
  10. package/dist/components/Shared.js +55 -0
  11. package/dist/data/derive.d.ts +21 -0
  12. package/dist/data/derive.js +47 -0
  13. package/dist/data/draftRules.d.ts +10 -0
  14. package/dist/data/draftRules.js +20 -0
  15. package/dist/data/engine/engineDataSource.d.ts +194 -0
  16. package/dist/data/engine/engineDataSource.js +836 -0
  17. package/dist/data/engine/errors.d.ts +13 -0
  18. package/dist/data/engine/errors.js +21 -0
  19. package/dist/data/engine/mapping.d.ts +172 -0
  20. package/dist/data/engine/mapping.js +1487 -0
  21. package/dist/data/engine/protocol.d.ts +910 -0
  22. package/dist/data/engine/protocol.js +67 -0
  23. package/dist/data/index.d.ts +18 -0
  24. package/dist/data/index.js +23 -0
  25. package/dist/data/mock/devSource.d.ts +7 -0
  26. package/dist/data/mock/devSource.js +10 -0
  27. package/dist/data/mock/fixtures-analytics.d.ts +7 -0
  28. package/dist/data/mock/fixtures-analytics.js +123 -0
  29. package/dist/data/mock/fixtures-briefs.d.ts +4 -0
  30. package/dist/data/mock/fixtures-briefs.js +230 -0
  31. package/dist/data/mock/fixtures-content.d.ts +4 -0
  32. package/dist/data/mock/fixtures-content.js +89 -0
  33. package/dist/data/mock/fixtures-digests.d.ts +3 -0
  34. package/dist/data/mock/fixtures-digests.js +40 -0
  35. package/dist/data/mock/fixtures-drafts.d.ts +2 -0
  36. package/dist/data/mock/fixtures-drafts.js +74 -0
  37. package/dist/data/mock/fixtures-gate.d.ts +3 -0
  38. package/dist/data/mock/fixtures-gate.js +49 -0
  39. package/dist/data/mock/fixtures-hooks.d.ts +2 -0
  40. package/dist/data/mock/fixtures-hooks.js +103 -0
  41. package/dist/data/mock/fixtures-inbox.d.ts +11 -0
  42. package/dist/data/mock/fixtures-inbox.js +213 -0
  43. package/dist/data/mock/fixtures-links.d.ts +5 -0
  44. package/dist/data/mock/fixtures-links.js +89 -0
  45. package/dist/data/mock/fixtures-memory.d.ts +3 -0
  46. package/dist/data/mock/fixtures-memory.js +148 -0
  47. package/dist/data/mock/fixtures-ops.d.ts +15 -0
  48. package/dist/data/mock/fixtures-ops.js +403 -0
  49. package/dist/data/mock/fixtures-optimise.d.ts +18 -0
  50. package/dist/data/mock/fixtures-optimise.js +221 -0
  51. package/dist/data/mock/fixtures-pacing.d.ts +5 -0
  52. package/dist/data/mock/fixtures-pacing.js +285 -0
  53. package/dist/data/mock/fixtures-publish.d.ts +2 -0
  54. package/dist/data/mock/fixtures-publish.js +129 -0
  55. package/dist/data/mock/fixtures-store.d.ts +10 -0
  56. package/dist/data/mock/fixtures-store.js +13 -0
  57. package/dist/data/mock/fixtures-variants.d.ts +5 -0
  58. package/dist/data/mock/fixtures-variants.js +138 -0
  59. package/dist/data/mock/fixtures.d.ts +6 -0
  60. package/dist/data/mock/fixtures.js +420 -0
  61. package/dist/data/mock/mockDataSource.d.ts +98 -0
  62. package/dist/data/mock/mockDataSource.js +903 -0
  63. package/dist/data/money.d.ts +14 -0
  64. package/dist/data/money.js +36 -0
  65. package/dist/data/platforms.d.ts +12 -0
  66. package/dist/data/platforms.js +26 -0
  67. package/dist/data/source.d.ts +196 -0
  68. package/dist/data/source.js +1 -0
  69. package/dist/data/time.d.ts +23 -0
  70. package/dist/data/time.js +86 -0
  71. package/dist/data/types.d.ts +1092 -0
  72. package/dist/data/types.js +3 -0
  73. package/dist/env.d.ts +59 -0
  74. package/dist/hooks/useAsync.d.ts +19 -0
  75. package/dist/hooks/useAsync.js +25 -0
  76. package/dist/index.d.ts +56 -0
  77. package/dist/index.js +58 -0
  78. package/dist/lib/host.d.ts +21 -0
  79. package/dist/lib/host.js +32 -0
  80. package/dist/lib/shell.d.ts +156 -0
  81. package/dist/lib/shell.js +16 -0
  82. package/dist/screens/AccountsScreen.d.ts +8 -0
  83. package/dist/screens/AccountsScreen.js +100 -0
  84. package/dist/screens/AnalyticsScreen.d.ts +7 -0
  85. package/dist/screens/AnalyticsScreen.js +64 -0
  86. package/dist/screens/ApprovalsScreen.d.ts +10 -0
  87. package/dist/screens/ApprovalsScreen.js +144 -0
  88. package/dist/screens/BriefsScreen.d.ts +7 -0
  89. package/dist/screens/BriefsScreen.js +83 -0
  90. package/dist/screens/CalendarScreen.d.ts +10 -0
  91. package/dist/screens/CalendarScreen.js +143 -0
  92. package/dist/screens/CampaignsScreen.d.ts +7 -0
  93. package/dist/screens/CampaignsScreen.js +52 -0
  94. package/dist/screens/ChannelMemoryScreen.d.ts +7 -0
  95. package/dist/screens/ChannelMemoryScreen.js +109 -0
  96. package/dist/screens/ContentScreen.d.ts +7 -0
  97. package/dist/screens/ContentScreen.js +64 -0
  98. package/dist/screens/DigestsScreen.d.ts +7 -0
  99. package/dist/screens/DigestsScreen.js +53 -0
  100. package/dist/screens/DraftsScreen.d.ts +10 -0
  101. package/dist/screens/DraftsScreen.js +108 -0
  102. package/dist/screens/FailuresScreen.d.ts +8 -0
  103. package/dist/screens/FailuresScreen.js +111 -0
  104. package/dist/screens/HookTestsScreen.d.ts +7 -0
  105. package/dist/screens/HookTestsScreen.js +54 -0
  106. package/dist/screens/InboxScreen.d.ts +12 -0
  107. package/dist/screens/InboxScreen.js +76 -0
  108. package/dist/screens/LinksScreen.d.ts +7 -0
  109. package/dist/screens/LinksScreen.js +52 -0
  110. package/dist/screens/PacingScreen.d.ts +7 -0
  111. package/dist/screens/PacingScreen.js +88 -0
  112. package/dist/screens/PublishJobsScreen.d.ts +7 -0
  113. package/dist/screens/PublishJobsScreen.js +66 -0
  114. package/dist/screens/SuggestionsScreen.d.ts +8 -0
  115. package/dist/screens/SuggestionsScreen.js +94 -0
  116. package/dist/screens/TextGateScreen.d.ts +7 -0
  117. package/dist/screens/TextGateScreen.js +111 -0
  118. package/dist/screens/ThreadsScreen.d.ts +8 -0
  119. package/dist/screens/ThreadsScreen.js +113 -0
  120. package/dist/screens/TypedBriefsScreen.d.ts +7 -0
  121. package/dist/screens/TypedBriefsScreen.js +75 -0
  122. package/dist/screens/labels.d.ts +121 -0
  123. package/dist/screens/labels.js +272 -0
  124. package/package.json +56 -3
@@ -0,0 +1,14 @@
1
+ /** Thousands grouping for a plain integer text, for example "18450" to "18,450". A leading "-" is kept. */
2
+ export declare function groupDigits(text: string): string;
3
+ /**
4
+ * Micro-units as currency text with six decimals, for example 2500000 to "2.500000" and -400000 to
5
+ * "-0.400000". Callers add the currency code. Exact for every safe integer.
6
+ */
7
+ export declare function formatMicro(micro: number): string;
8
+ /**
9
+ * Thousandths as exact decimal text, for example 1500 to "1.5", 250 to "0.25" and 4000 to "4". Works on the
10
+ * digits of the integer, so no float is used. Callers pass safe integers only.
11
+ */
12
+ export declare function formatMilli(milli: number): string;
13
+ /** The "YYYY-MM" month of a UTC instant, the form the allowance and report commands take. */
14
+ export declare function monthOfInstant(utc: string): string;
@@ -0,0 +1,36 @@
1
+ // Exact money text. Amounts are integer micro-units (1,000,000 = 1 currency unit). Formatting works on
2
+ // the decimal digits of the integer, so no float arithmetic is used. The data layer rejects any amount
3
+ // that is not a safe integer before it reaches a screen (see mapping.ts, "Suggestions and spend").
4
+ const MICRO_PER_UNIT_DIGITS = 6;
5
+ /** Thousands grouping for a plain integer text, for example "18450" to "18,450". A leading "-" is kept. */
6
+ export function groupDigits(text) {
7
+ const sign = text.startsWith("-") ? "-" : "";
8
+ const body = sign === "" ? text : text.slice(1);
9
+ return sign + body.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
10
+ }
11
+ /**
12
+ * Micro-units as currency text with six decimals, for example 2500000 to "2.500000" and -400000 to
13
+ * "-0.400000". Callers add the currency code. Exact for every safe integer.
14
+ */
15
+ export function formatMicro(micro) {
16
+ const negative = micro < 0;
17
+ const digits = String(Math.abs(micro)).padStart(MICRO_PER_UNIT_DIGITS + 1, "0");
18
+ const whole = digits.slice(0, -MICRO_PER_UNIT_DIGITS);
19
+ const fraction = digits.slice(-MICRO_PER_UNIT_DIGITS);
20
+ return `${negative ? "-" : ""}${groupDigits(whole)}.${fraction}`;
21
+ }
22
+ /**
23
+ * Thousandths as exact decimal text, for example 1500 to "1.5", 250 to "0.25" and 4000 to "4". Works on the
24
+ * digits of the integer, so no float is used. Callers pass safe integers only.
25
+ */
26
+ export function formatMilli(milli) {
27
+ const negative = milli < 0;
28
+ const digits = String(Math.abs(milli)).padStart(4, "0");
29
+ const whole = String(Number(digits.slice(0, -3)));
30
+ const fraction = digits.slice(-3).replace(/0+$/, "");
31
+ return `${negative ? "-" : ""}${groupDigits(whole)}${fraction === "" ? "" : `.${fraction}`}`;
32
+ }
33
+ /** The "YYYY-MM" month of a UTC instant, the form the allowance and report commands take. */
34
+ export function monthOfInstant(utc) {
35
+ return utc.slice(0, 7);
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { SocialPlatform } from "./types.js";
2
+ /**
3
+ * Every account platform the engine knows, in the engine's order: PLATFORMS in
4
+ * crates/protocol/src/accounts.rs:22-33. Keep in step with the SocialPlatform union in types.ts.
5
+ */
6
+ export declare const SOCIAL_PLATFORMS: readonly SocialPlatform[];
7
+ /** True when the id is an account platform the desktop shows. A draft-only id such as "reddit" is not one. */
8
+ export declare function isSocialPlatform(value: string): value is SocialPlatform;
9
+ /** Draft-only platform ids the engine accepts beside the account vocabulary (drafts.rs, DRAFT_ONLY_PLATFORMS). */
10
+ export declare const DRAFT_ONLY_PLATFORMS: readonly string[];
11
+ /** True when the id is a platform the desktop shows: an account platform or a draft-only platform. */
12
+ export declare function isKnownPlatform(value: string): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Every account platform the engine knows, in the engine's order: PLATFORMS in
3
+ * crates/protocol/src/accounts.rs:22-33. Keep in step with the SocialPlatform union in types.ts.
4
+ */
5
+ export const SOCIAL_PLATFORMS = [
6
+ "instagram",
7
+ "facebook",
8
+ "threads",
9
+ "youtube",
10
+ "tiktok",
11
+ "x",
12
+ "linkedin",
13
+ "pinterest",
14
+ "bluesky",
15
+ "mastodon",
16
+ ];
17
+ /** True when the id is an account platform the desktop shows. A draft-only id such as "reddit" is not one. */
18
+ export function isSocialPlatform(value) {
19
+ return SOCIAL_PLATFORMS.includes(value);
20
+ }
21
+ /** Draft-only platform ids the engine accepts beside the account vocabulary (drafts.rs, DRAFT_ONLY_PLATFORMS). */
22
+ export const DRAFT_ONLY_PLATFORMS = ["reddit"];
23
+ /** True when the id is a platform the desktop shows: an account platform or a draft-only platform. */
24
+ export function isKnownPlatform(value) {
25
+ return isSocialPlatform(value) || DRAFT_ONLY_PLATFORMS.includes(value);
26
+ }
@@ -0,0 +1,196 @@
1
+ import type { Account, AccountDisconnect, Campaign, CampaignAttribution, ChannelLink, DataError as BaseDataError, DataErrorCode as BaseDataErrorCode, DateRange, Digest, FailureItem, FailureResolution, HookTestSet, ModerationCapability, PublishJob, PublishJobRecord, ReconcileResult, ManualDraft, OperatorScope, OptimiseProposal, RejectDecision, SpendAllowance, SpendPrice, SpendReport, ThreadItem, TrackedLink, ContentDetail, ContentExperimentSummary, ContentRecord, DigestApplication, ResearchDigest, DeliveryEnvelope, DispatchAttempt, GateCheck, GatePolicy, GateRecord, OutgoingBrief, OutgoingBriefDetail, ChannelMemoryDetail, ChannelMemoryRevision, ProofRecord, ProofStatus, TypedBrief, BriefVariant, AnalyticsObservation, AnalyticsPost, MetricImport, HeartbeatStatus, RestoreReceiptList, InboxListing, InboxRaw, InboxStats, PublishNextView, PublishQuota } from "./types.js";
2
+ /**
3
+ * Codes the engine returns that types.ts does not list. They are declared here, not in types.ts,
4
+ * so this change stays inside the engine data source. Move them into types.ts with the next change
5
+ * to that file.
6
+ * foreign_scope the scope matches no stored channel
7
+ * engine_unavailable the engine has no method or field for this yet; the message names it
8
+ * engine_error any other engine error; `engineCode` carries the engine's own code
9
+ */
10
+ export type EngineOnlyErrorCode = "foreign_scope" | "engine_unavailable" | "engine_error";
11
+ export type DataErrorCode = BaseDataErrorCode | EngineOnlyErrorCode;
12
+ export interface DataError extends Omit<BaseDataError, "code"> {
13
+ code: DataErrorCode;
14
+ /** Set on engine_error: the engine's own error code. */
15
+ engineCode?: string;
16
+ }
17
+ /**
18
+ * A typed result. Every value in types.ts still fits: the mock's Result<T> is assignable to this.
19
+ */
20
+ export type Result<T> = {
21
+ ok: true;
22
+ value: T;
23
+ } | {
24
+ ok: false;
25
+ error: DataError;
26
+ };
27
+ /**
28
+ * An account the engine holds that the desktop cannot map yet: its platform is not in SOCIAL_PLATFORMS, or a
29
+ * state is not shown. It is listed apart from the mapped accounts, with its raw id and the reason. Never dropped.
30
+ */
31
+ export interface UnmappedAccount {
32
+ /** The desktop account id the account would have had. */
33
+ id: string;
34
+ /** The engine's raw platform id, shown as stored. */
35
+ platform: string;
36
+ reason: string;
37
+ }
38
+ /**
39
+ * The seam between the screens and the daemon. The screens import only this interface. Today the
40
+ * only implementation is MockDataSource. The daemon client implements the same methods later, so
41
+ * no screen changes when it lands.
42
+ *
43
+ * Rule carried from the daemon: approve and reject fail on any mismatch between the revision or
44
+ * account the operator saw and the job the daemon holds. Nothing is applied on mismatch.
45
+ */
46
+ export interface DataSource {
47
+ /** "mock" shows the Mock data banner on every screen. */
48
+ readonly kind: "mock" | "daemon";
49
+ /** The daemon's clock. Countdowns and "today" are computed from this, never from the browser. */
50
+ nowUtc(): string;
51
+ listAccounts(): Promise<Account[]>;
52
+ /** The accounts the last listAccounts could not map. Optional: the mock holds none. Read after listAccounts. */
53
+ unmappedAccounts?(): readonly UnmappedAccount[];
54
+ listJobs(range: DateRange): Promise<PublishJob[]>;
55
+ getJob(jobId: string): Promise<PublishJob | null>;
56
+ approve(jobId: string, revision: number, accountId: string): Promise<Result<PublishJob>>;
57
+ reject(jobId: string, decision: RejectDecision): Promise<Result<PublishJob>>;
58
+ pauseAccount(accountId: string, reason: string): Promise<Result<Account>>;
59
+ resumeAccount(accountId: string): Promise<Result<Account>>;
60
+ /**
61
+ * Ends the connection to the platform and cancels the account's queued and waiting posts. The engine stores no
62
+ * token, so the revocation is its state change; the person also revokes access in the platform's own settings.
63
+ * Does not delete data (a deletion request does).
64
+ */
65
+ disconnectAccount(accountId: string): Promise<Result<AccountDisconnect>>;
66
+ listFailures(): Promise<FailureItem[]>;
67
+ /** Asks the platform whether an outcome_unknown post exists. A read only; never publishes. */
68
+ reconcile(jobId: string): Promise<Result<ReconcileResult>>;
69
+ /**
70
+ * Records how a failure ended. The daemon enforces the rules: a published or cancelled outcome
71
+ * needs a matching reconcile result, acceptance as lost needs a note, and an alert takes only
72
+ * "acknowledged".
73
+ */
74
+ resolveFailure(failureId: string, resolution: FailureResolution, note: string): Promise<Result<FailureItem>>;
75
+ listDigest(): Promise<Digest>;
76
+ /** Every publish job of one channel, newest first. An empty list is a state, not an error. */
77
+ listPublishJobs(scopeId: string): Promise<PublishJobRecord[]>;
78
+ listThreads(): Promise<ThreadItem[]>;
79
+ resolveThread(threadId: string): Promise<Result<ThreadItem>>;
80
+ /** Snoozes a thread until a UTC instant that must be in the future. */
81
+ snoozeThread(threadId: string, untilUtc: string): Promise<Result<ThreadItem>>;
82
+ listModerationCapabilities(): Promise<ModerationCapability[]>;
83
+ listDrafts(): Promise<ManualDraft[]>;
84
+ /**
85
+ * Approves a drafted draft at exactly `revision`. The caller supplies the disclosure phrases; the
86
+ * engine has no built-in list. The engine's gate decides, and a refusal changes nothing.
87
+ */
88
+ approveDraft(draftId: string, revision: number, disclosurePhrases: readonly string[]): Promise<Result<ManualDraft>>;
89
+ /** Records the operator's post. The link must be https on an allowed host for the platform. */
90
+ markDraftPosted(draftId: string, revision: number, postedUrl: string): Promise<Result<ManualDraft>>;
91
+ /** Ends a drafted or approved draft with a reason. The reason is required. */
92
+ skipDraft(draftId: string, revision: number, reason: string): Promise<Result<ManualDraft>>;
93
+ listScopes(): Promise<OperatorScope[]>;
94
+ listProposals(scopeId: string): Promise<OptimiseProposal[]>;
95
+ /** Null when the suggestion is not in this scope. */
96
+ getProposal(scopeId: string, proposalId: string): Promise<OptimiseProposal | null>;
97
+ /**
98
+ * Accepts a suggestion at exactly `expectedRevision`. The engine creates one draft calendar entry and
99
+ * records its id. The entry still needs a real caption and approval; nothing is approved here.
100
+ */
101
+ acceptProposal(scopeId: string, proposalId: string, expectedRevision: number): Promise<Result<OptimiseProposal>>;
102
+ /** Dismisses a suggestion at exactly `expectedRevision`. Creates no calendar entry. */
103
+ dismissProposal(scopeId: string, proposalId: string, expectedRevision: number): Promise<Result<OptimiseProposal>>;
104
+ /** Null when no allowance is set for the month. That is a state, not an error. */
105
+ getAllowance(scopeId: string, month: string): Promise<SpendAllowance | null>;
106
+ /** Null when no allowance is set for the month. That is a state, not an error. */
107
+ spendReport(scopeId: string, month: string): Promise<SpendReport | null>;
108
+ listPrices(): Promise<SpendPrice[]>;
109
+ listCampaigns(scopeId: string): Promise<Campaign[]>;
110
+ /** Every tracked link of one campaign in the scope. An empty list is a state, not an error. */
111
+ listCampaignLinks(scopeId: string, campaignId: string): Promise<TrackedLink[]>;
112
+ /** Null when the campaign is not in this scope. */
113
+ campaignAttribution(scopeId: string, campaignId: string): Promise<CampaignAttribution | null>;
114
+ /**
115
+ * Every tracked link in one scope, across its campaigns, each with its campaign's name and visit count. The
116
+ * campaign order is the engine's, then variant, platform and link id. An empty list is a state, not an error.
117
+ */
118
+ listChannelLinks(scopeId: string): Promise<ChannelLink[]>;
119
+ listHookTests(scopeId: string): Promise<HookTestSet[]>;
120
+ /** Null when no set with this id is in the scope. */
121
+ getHookTest(scopeId: string, setId: string): Promise<HookTestSet | null>;
122
+ /** Every digest in one scope, ordered by digest date then id. */
123
+ listDigests(scopeId: string): Promise<ResearchDigest[]>;
124
+ /** Null when no digest with this id is in the scope. */
125
+ getDigest(scopeId: string, digestId: string): Promise<ResearchDigest | null>;
126
+ /** Which digest entries became draft calendar entries. Null when the digest is not in the scope. */
127
+ digestApplications(scopeId: string, digestId: string): Promise<DigestApplication[] | null>;
128
+ /** Every content record in one scope, ordered by content id. */
129
+ listContent(scopeId: string): Promise<ContentRecord[]>;
130
+ /** A record with its variants. Null when no record with this id is in the scope. */
131
+ getContent(scopeId: string, contentId: string): Promise<ContentDetail | null>;
132
+ /** The engine's variant comparison for one record. Null when the record is not in the scope. */
133
+ contentExperimentSummary(scopeId: string, contentId: string): Promise<ContentExperimentSummary | null>;
134
+ /** The latest memory revision with its document. Null when none has been written: a state, not an error. */
135
+ channelMemory(scopeId: string): Promise<ChannelMemoryDetail | null>;
136
+ /** Every stored memory revision, newest first, without documents. An empty list when none is stored. */
137
+ channelMemoryHistory(scopeId: string): Promise<ChannelMemoryRevision[]>;
138
+ /** The proofs of one scope, sorted by answer id. A null status lists every status. */
139
+ listProofs(scopeId: string, status: ProofStatus | null): Promise<ProofRecord[]>;
140
+ /** The latest gate policy of the scope. Null when none is set, which means nothing can pass. That is a state, not an error. */
141
+ gatePolicy(scopeId: string): Promise<GatePolicy | null>;
142
+ /**
143
+ * A dry-run check of one script with no claim annotations. Stores nothing. A refusal (for example hidden
144
+ * characters, or no policy) is a Result, not a throw.
145
+ */
146
+ checkText(scopeId: string, subjectId: string, text: string): Promise<Result<GateCheck>>;
147
+ /** The stored verdict records of one subject, newest first. A subject with none is an empty list. */
148
+ gateRecords(scopeId: string, subjectId: string): Promise<GateRecord[]>;
149
+ /** Every outgoing brief in one scope, ordered by request id. */
150
+ listBriefs(scopeId: string): Promise<OutgoingBrief[]>;
151
+ /** Null when no brief with this request id is in the scope. */
152
+ getBrief(scopeId: string, requestId: string): Promise<OutgoingBriefDetail | null>;
153
+ /** Every accepted delivery in one scope. */
154
+ listDeliveries(scopeId: string): Promise<DeliveryEnvelope[]>;
155
+ /** Null when no envelope with this id is in the scope. */
156
+ getDelivery(scopeId: string, envelopeId: string): Promise<DeliveryEnvelope | null>;
157
+ /** The dispatch attempts of one brief, oldest first. An empty list is a state, not an error. */
158
+ listDispatchAttempts(scopeId: string, requestId: string): Promise<DispatchAttempt[]>;
159
+ /** Every typed brief in one scope, ordered by brief id. */
160
+ listTypedBriefs(scopeId: string): Promise<TypedBrief[]>;
161
+ /** Null when no brief with this id is in the scope. */
162
+ getTypedBrief(scopeId: string, briefId: string): Promise<TypedBrief | null>;
163
+ /** The variants of one brief, ordered by platform then variant id. An empty list is a state, not an error. */
164
+ listBriefVariants(scopeId: string, briefId: string): Promise<BriefVariant[]>;
165
+ /** Every published post in one scope with its latest stored metrics. An empty list is a state, not an error. */
166
+ listAnalyticsPosts(scopeId: string): Promise<AnalyticsPost[]>;
167
+ /** Every stored observation of one post in the scope, oldest first. An empty list is a state, not an error. */
168
+ listAnalyticsObservations(scopeId: string, jobId: string): Promise<AnalyticsObservation[]>;
169
+ /** Every committed manual import in one scope. An empty list is a state, not an error. */
170
+ listMetricImports(scopeId: string): Promise<MetricImport[]>;
171
+ /**
172
+ * The daemon heartbeat record's state at `nowUtc`, against `staleAfterSecs`. The ages and states come from the engine
173
+ * as given. An empty list is a state, and it is not a sign of health.
174
+ */
175
+ heartbeatStatus(nowUtc: string, staleAfterSecs: number): Promise<HeartbeatStatus>;
176
+ /** Every restore receipt, newest first, with the receipt files that could not be read. */
177
+ listRestoreReceipts(): Promise<RestoreReceiptList>;
178
+ /** The newest page of events of one channel account, or of the unmatched events when scopeId is null. */
179
+ listInbox(scopeId: string | null): Promise<InboxListing>;
180
+ /** The raw item JSON of one event. Null when no event with this id is in the scope. */
181
+ getInboxEvent(scopeId: string | null, eventId: string): Promise<InboxRaw | null>;
182
+ /** Event and delivery counts as the daemon stores them. Whole store, not per channel. */
183
+ inboxStats(): Promise<InboxStats>;
184
+ /** What may run now on every account of one channel, and what may not, with reasons. Scope ids come from listScopes. */
185
+ listPublishNext(scopeId: string): Promise<PublishNextView>;
186
+ /**
187
+ * One account's pacing: the limits in force, the pause, the last create and the platform wait. Throws on a failed
188
+ * read, so the screen shows the error for that account only.
189
+ */
190
+ getPublishQuota(scopeId: string, platform: string, remoteAccount: string): Promise<PublishQuota>;
191
+ /** Present only on the mock. Lets the preview simulate an edit made on another device. */
192
+ readonly mock?: MockControls;
193
+ }
194
+ export interface MockControls {
195
+ simulateCaptionChange(jobId: string, caption: string): Promise<Result<PublishJob>>;
196
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ /** Calendar date ("YYYY-MM-DD") of an instant as seen in a zone. */
2
+ export declare function dateKeyInZone(utc: string, timeZone: string): string;
3
+ /** Wall-clock time "HH:MM" of an instant in a zone. */
4
+ export declare function timeInZone(utc: string, timeZone: string): string;
5
+ /** Zone abbreviation for display, for example "GMT+1" or "EDT". */
6
+ export declare function zoneAbbr(utc: string, timeZone: string): string;
7
+ /** Numeric UTC offset of an instant in a zone, for example "+01:00". */
8
+ export declare function zoneOffset(utc: string, timeZone: string): string;
9
+ /** "Tue 13 Oct" for a date key. */
10
+ export declare function dayLabel(dateKey: string): string;
11
+ /** Full instant label, for example "Tue 13 Oct 09:00 GMT+1 (Europe/Lisbon, +01:00)". */
12
+ export declare function instantLabel(utc: string, timeZone: string): string;
13
+ export declare function dateKeyAdd(dateKey: string, days: number): string;
14
+ /** 0 for Monday through 6 for Sunday. */
15
+ export declare function weekdayIndex(dateKey: string): number;
16
+ export declare function mondayOf(dateKey: string): string;
17
+ export declare function weekDays(mondayKey: string): string[];
18
+ export declare function startOfDateUtc(dateKey: string): string;
19
+ /** Remaining time from now until a target, for example "11 h 50 min". Returns "now" when past. */
20
+ export declare function remaining(targetUtc: string, nowUtc: string): string;
21
+ /** Whole days from now until a calendar date (UTC midnight), rounded up. Negative when past. */
22
+ export declare function daysUntil(dateKey: string, nowUtc: string): number;
23
+ export declare function formatCount(n: number): string;
@@ -0,0 +1,86 @@
1
+ // Time helpers. Instants are UTC ISO strings. Local wall-clock values come from Intl with the
2
+ // account's IANA zone. Date keys are "YYYY-MM-DD" in the zone they were computed for.
3
+ const DAY_MS = 86_400_000;
4
+ const formatterCache = new Map();
5
+ function formatter(key, make) {
6
+ let f = formatterCache.get(key);
7
+ if (!f) {
8
+ f = make();
9
+ formatterCache.set(key, f);
10
+ }
11
+ return f;
12
+ }
13
+ /** Calendar date ("YYYY-MM-DD") of an instant as seen in a zone. */
14
+ export function dateKeyInZone(utc, timeZone) {
15
+ const f = formatter(`date|${timeZone}`, () => new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }));
16
+ const parts = f.formatToParts(new Date(utc));
17
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
18
+ return `${get("year")}-${get("month")}-${get("day")}`;
19
+ }
20
+ /** Wall-clock time "HH:MM" of an instant in a zone. */
21
+ export function timeInZone(utc, timeZone) {
22
+ const f = formatter(`time|${timeZone}`, () => new Intl.DateTimeFormat("en-GB", { timeZone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }));
23
+ return f.format(new Date(utc));
24
+ }
25
+ /** Zone abbreviation for display, for example "GMT+1" or "EDT". */
26
+ export function zoneAbbr(utc, timeZone) {
27
+ const f = formatter(`abbr|${timeZone}`, () => new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "short" }));
28
+ return f.formatToParts(new Date(utc)).find((p) => p.type === "timeZoneName")?.value ?? timeZone;
29
+ }
30
+ /** Numeric UTC offset of an instant in a zone, for example "+01:00". */
31
+ export function zoneOffset(utc, timeZone) {
32
+ const f = formatter(`offset|${timeZone}`, () => new Intl.DateTimeFormat("en-GB", { timeZone, timeZoneName: "longOffset" }));
33
+ const raw = f.formatToParts(new Date(utc)).find((p) => p.type === "timeZoneName")?.value ?? "GMT";
34
+ const offset = raw.replace("GMT", "");
35
+ return offset === "" ? "+00:00" : offset;
36
+ }
37
+ /** "Tue 13 Oct" for a date key. */
38
+ export function dayLabel(dateKey) {
39
+ const f = formatter("dayLabel", () => new Intl.DateTimeFormat("en-GB", { timeZone: "UTC", weekday: "short", day: "numeric", month: "short" }));
40
+ return f.format(new Date(`${dateKey}T12:00:00Z`));
41
+ }
42
+ /** Full instant label, for example "Tue 13 Oct 09:00 GMT+1 (Europe/Lisbon, +01:00)". */
43
+ export function instantLabel(utc, timeZone) {
44
+ const key = dateKeyInZone(utc, timeZone);
45
+ return `${dayLabel(key)} ${timeInZone(utc, timeZone)} ${zoneAbbr(utc, timeZone)} (${timeZone}, ${zoneOffset(utc, timeZone)})`;
46
+ }
47
+ export function dateKeyAdd(dateKey, days) {
48
+ const ms = Date.parse(`${dateKey}T00:00:00Z`) + days * DAY_MS;
49
+ return new Date(ms).toISOString().slice(0, 10);
50
+ }
51
+ /** 0 for Monday through 6 for Sunday. */
52
+ export function weekdayIndex(dateKey) {
53
+ const day = new Date(`${dateKey}T00:00:00Z`).getUTCDay();
54
+ return (day + 6) % 7;
55
+ }
56
+ export function mondayOf(dateKey) {
57
+ return dateKeyAdd(dateKey, -weekdayIndex(dateKey));
58
+ }
59
+ export function weekDays(mondayKey) {
60
+ return Array.from({ length: 7 }, (_, i) => dateKeyAdd(mondayKey, i));
61
+ }
62
+ export function startOfDateUtc(dateKey) {
63
+ return `${dateKey}T00:00:00Z`;
64
+ }
65
+ /** Remaining time from now until a target, for example "11 h 50 min". Returns "now" when past. */
66
+ export function remaining(targetUtc, nowUtc) {
67
+ const ms = Date.parse(targetUtc) - Date.parse(nowUtc);
68
+ if (ms <= 0)
69
+ return "now";
70
+ const totalMin = Math.ceil(ms / 60_000);
71
+ const hours = Math.floor(totalMin / 60);
72
+ const mins = totalMin % 60;
73
+ if (hours === 0)
74
+ return `${mins} min`;
75
+ if (hours < 48)
76
+ return `${hours} h ${mins} min`;
77
+ return `${Math.floor(hours / 24)} days`;
78
+ }
79
+ /** Whole days from now until a calendar date (UTC midnight), rounded up. Negative when past. */
80
+ export function daysUntil(dateKey, nowUtc) {
81
+ const ms = Date.parse(`${dateKey}T00:00:00Z`) - Date.parse(nowUtc);
82
+ return Math.ceil(ms / DAY_MS);
83
+ }
84
+ export function formatCount(n) {
85
+ return n.toLocaleString("en-GB");
86
+ }