@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,54 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, EmptyState, Field, PageShell, SkeletonRows, Table, TableBody, TableCaption, TableCell, TableHead, TableHeaderCell, TableRow } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { groupDigits, instantLabel, } from "../data/index.js";
8
+ import { HOOK_ROLE_LABEL, HOOK_STATE_LABEL } from "./labels.js";
9
+ /** An instant from the engine, in UTC, or the words "Not reported" when the engine holds none. */
10
+ function instantOrNone(utc) {
11
+ return utc === null ? "Not reported" : instantLabel(utc, "UTC");
12
+ }
13
+ /** The role of one opening. Only a decided set has roles; an open set's openings are not decided. */
14
+ function roleText(opening) {
15
+ return opening.role === null ? "Not decided" : HOOK_ROLE_LABEL[opening.role];
16
+ }
17
+ export function HookTestsScreen({ source, rev }) {
18
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
19
+ const [picked, setPicked] = useState(null);
20
+ const scopeList = scopes.value ?? [];
21
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
22
+ return (_jsxs(PageShell, { title: "Hook tests", subtitle: "Read only. Variant sets: one body with several openings, each published as a trial reel, and the winner once the set is decided. Nothing here creates a set, binds a job or reads a set.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "Hook tests are kept per channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => setPicked(id) }), _jsx(HookTestView, { source: source, rev: rev, scopeId: scopeId }, scopeId)] }))] }));
23
+ }
24
+ function HookTestView({ source, rev, scopeId }) {
25
+ const { state, reload, value } = useAsync(() => source.listHookTests(scopeId), [source, rev, scopeId]);
26
+ const [picked, setPicked] = useState(null);
27
+ if (state.status === "error") {
28
+ return _jsx(LoadError, { message: "Could not read the hook tests from the daemon.", onRetry: reload });
29
+ }
30
+ if (value === null)
31
+ return _jsx(SkeletonRows, { label: "Loading hook tests", rows: 3 });
32
+ if (value.length === 0) {
33
+ return (_jsx(EmptyState, { title: "No hook tests yet", children: _jsx("p", { children: "This channel has no hook test. A set is made on the daemon host, not here. This is not an error." }) }));
34
+ }
35
+ const selected = value.find((s) => s.id === picked) ?? value[0];
36
+ return (_jsxs(_Fragment, { children: [_jsxs("section", { "aria-labelledby": "hook-list-title", children: [_jsxs("h2", { id: "hook-list-title", className: "op-section-title", children: ["Hook tests (", value.length, ")"] }), _jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "hook-list-title", children: [_jsx(TableCaption, { children: "Variant sets in this channel" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Set" }), _jsx(TableHeaderCell, { children: "Brief" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Metric and window" }), _jsx(TableHeaderCell, { align: "end", children: "Openings bound" }), _jsx(TableHeaderCell, { children: "Created (UTC)" })] }) }), _jsx(TableBody, { children: value.map((set) => {
37
+ const badge = HOOK_STATE_LABEL[set.state];
38
+ const bound = set.openings.filter((o) => o.jobId !== null).length;
39
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, className: "op-mono", children: set.id }), _jsx(TableCell, { children: set.briefRef }), _jsx(TableCell, { children: _jsx(Badge, { tone: badge.tone, children: badge.text }) }), _jsxs(TableCell, { children: [set.metric, ", ", set.window] }), _jsxs(TableCell, { numeric: true, children: [bound, " of ", set.openings.length] }), _jsx(TableCell, { className: "op-mono", children: instantLabel(set.createdAtUtc, "UTC") })] }, set.id));
40
+ }) })] }) })] }), _jsxs("section", { "aria-labelledby": "hook-detail-heading", children: [_jsx("h2", { id: "hook-detail-heading", className: "op-section-title", children: "Selected hook test" }), _jsx(Field, { label: "Hook test", children: ({ id, "aria-describedby": describedBy }) => (_jsx("select", { id: id, "aria-describedby": describedBy, className: "op-select", value: selected.id, onChange: (e) => setPicked(e.target.value), children: value.map((set) => (_jsxs("option", { value: set.id, children: [set.briefRef, " (", set.id, ")"] }, set.id))) })) }), _jsx(HookTestDetail, { source: source, rev: rev, scopeId: scopeId, setId: selected.id }, selected.id)] })] }));
41
+ }
42
+ function HookTestDetail({ source, rev, scopeId, setId, }) {
43
+ const { state, reload } = useAsync(() => source.getHookTest(scopeId, setId), [source, rev, scopeId, setId]);
44
+ if (state.status === "error") {
45
+ return _jsx(LoadError, { message: "Could not read this hook test from the daemon.", onRetry: reload });
46
+ }
47
+ if (state.status === "loading")
48
+ return _jsx(SkeletonRows, { label: "Loading the hook test", rows: 3 });
49
+ const set = state.value;
50
+ if (set === null)
51
+ return _jsx("p", { className: "op-hint", children: "This hook test is no longer in this channel." });
52
+ const badge = HOOK_STATE_LABEL[set.state];
53
+ return (_jsxs("article", { className: "op-detail", "aria-labelledby": "hook-detail-title", children: [_jsx("h3", { id: "hook-detail-title", className: "op-detail__title", children: set.briefRef }), _jsx("div", { className: "op-badges", children: _jsx(Badge, { tone: badge.tone, children: badge.text }) }), set.state === "open" ? (_jsx("p", { className: "op-hint", children: "This set is open. Each opening is read 24 hours after its job is published. That reading happens on the server, not here. This screen shows the set as it stands and does not read it or decide it." })) : (_jsxs("p", { className: "op-hint", children: ["Decided ", instantOrNone(set.decidedAtUtc), ". Opening ", set.winnerIndex ?? "not recorded", " was promoted and the other openings were archived. Each keeps the number it was compared on, when it was observed and the pull that stored it."] })), _jsxs("dl", { className: "op-dl", children: [_jsx("dt", { children: "Metric and window" }), _jsxs("dd", { children: [set.metric, ", ", set.window] }), _jsx("dt", { children: "Revision" }), _jsx("dd", { children: set.revision }), _jsx("dt", { children: "Created" }), _jsx("dd", { children: instantLabel(set.createdAtUtc, "UTC") }), _jsx("dt", { children: "Last changed" }), _jsx("dd", { children: instantLabel(set.updatedAtUtc, "UTC") })] }), _jsx("h4", { className: "op-section-title", children: "Body" }), _jsx("div", { className: "op-caption", children: set.body }), _jsx("h4", { id: "hook-openings-title", className: "op-section-title", children: "Openings" }), _jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "hook-openings-title", children: [_jsx(TableCaption, { children: set.state === "open" ? "Openings and their bound jobs" : "Openings with their numbers and lineage" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Opening" }), _jsx(TableHeaderCell, { children: "Bound job" }), _jsx(TableHeaderCell, { children: "Role" }), _jsxs(TableHeaderCell, { align: "end", children: [set.metric, " value"] }), _jsx(TableHeaderCell, { children: "Observed (UTC)" }), _jsx(TableHeaderCell, { children: "Pull" }), _jsx(TableHeaderCell, { children: "Published (UTC)" })] }) }), _jsx(TableBody, { children: set.openings.map((opening) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { rowHeader: true, children: [opening.index, ". ", opening.text] }), _jsx(TableCell, { className: "op-mono", children: opening.jobId ?? "Not bound yet" }), _jsx(TableCell, { children: roleText(opening) }), _jsx(TableCell, { numeric: true, children: opening.value === null ? "Not read" : groupDigits(opening.value) }), _jsx(TableCell, { className: "op-mono", children: instantOrNone(opening.observedAtUtc) }), _jsx(TableCell, { className: "op-mono", children: opening.pullId ?? "None" }), _jsx(TableCell, { className: "op-mono", children: instantOrNone(opening.publishedAtUtc) })] }, opening.index))) })] }) })] }));
54
+ }
@@ -0,0 +1,12 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ }
6
+ /**
7
+ * The inbox of the signed webhook events. Every text, id and time in an event was written by the sender. The
8
+ * screen renders each one as a React text child: never as markup, a link, an attribute or a class. Keys are the
9
+ * engine's event ids. Nothing here sends a reply, marks an event or changes its state.
10
+ */
11
+ export declare function InboxScreen({ source, rev }: Props): import("react").JSX.Element;
12
+ export {};
@@ -0,0 +1,76 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, Button, EmptyState, PageShell, SkeletonRows, Table, TableBody, TableCaption, TableCell, TableHead, TableHeaderCell, TableRow } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { groupDigits, instantLabel, } from "../data/index.js";
8
+ import { inboxKindText, inboxStateText, platformName } from "./labels.js";
9
+ import { hostAppName } from "../appName.js";
10
+ /**
11
+ * The inbox of the signed webhook events. Every text, id and time in an event was written by the sender. The
12
+ * screen renders each one as a React text child: never as markup, a link, an attribute or a class. Keys are the
13
+ * engine's event ids. Nothing here sends a reply, marks an event or changes its state.
14
+ */
15
+ export function InboxScreen({ source, rev }) {
16
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
17
+ const stats = useAsync(() => source.inboxStats(), [source, rev]);
18
+ const [picked, setPicked] = useState(null);
19
+ const scopeList = scopes.value ?? [];
20
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
21
+ return (_jsxs(PageShell, { title: "Inbox", subtitle: "Read only. The signed webhook events the daemon stored for one channel account, newest first, and the events no account matched. Nothing here replies, marks, ignores or handles an event.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, _jsxs("p", { className: "op-hint", children: ["Event text is shown exactly as received, as plain text. ", hostAppName(), " sends no reply from this screen."] }), scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "The inbox is kept per channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => setPicked(id) }), stats.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the inbox counts from the daemon.", onRetry: stats.reload })) : stats.value === null ? (_jsx(SkeletonRows, { label: "Loading the inbox counts", rows: 1 })) : (_jsx(InboxCounts, { stats: stats.value, scopeId: scopeId })), _jsx(EventSection, { source: source, rev: rev, scopeId: scopeId, headingId: "inbox-scope-title", title: "Events of this channel", caption: "Webhook events bound to this channel account, newest first", emptyText: "Nothing has arrived for this channel yet." }, `scope:${scopeId}`)] })), _jsx(EventSection, { source: source, rev: rev, scopeId: null, headingId: "inbox-unmatched-title", title: "Not matched to an account", caption: "Webhook events no stored account matched, newest first", emptyText: "No event is waiting here: every stored event matched an account." }, "unmatched")] }));
22
+ }
23
+ /** The counts of the whole store, and this channel's counts by kind and state as stored. Counts are never rates. */
24
+ function InboxCounts({ stats, scopeId }) {
25
+ const channelGroups = stats.groups.filter((row) => !("readable" in row) && row.scopeId === scopeId);
26
+ const unreadableGroups = stats.groups.filter((row) => "readable" in row).length;
27
+ return (_jsxs("section", { "aria-labelledby": "inbox-counts-title", children: [_jsx("h2", { id: "inbox-counts-title", className: "op-section-title", children: "Counts" }), _jsxs("p", { className: "op-count", children: ["Whole store, all channels: ", groupDigits(String(stats.totalEvents)), " events stored,", " ", groupDigits(String(stats.unmatchedEvents)), " not matched to an account."] }), _jsxs("p", { className: "op-count", children: ["Deliveries stored: ", groupDigits(String(stats.deliveriesParsed)), " read,", " ", groupDigits(String(stats.deliveriesUnparseable)), " could not be read."] }), _jsxs("p", { className: "op-count", children: ["Deliveries refused for a bad signature (whole store): ", groupDigits(String(stats.refusedTotal)), "."] }), _jsx("p", { className: "op-hint", children: "The refusal count is not kept per channel. A refused delivery is never stored, so it adds no event." }), channelGroups.length === 0 ? (_jsx("p", { className: "op-hint", children: "No event is counted for this channel." })) : (_jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "inbox-counts-title", children: [_jsx(TableCaption, { children: "Events of this channel by kind and state, as stored" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Kind" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { align: "end", children: "Count" })] }) }), _jsx(TableBody, { children: channelGroups.map((group, index) => (_jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: inboxKindText(group.kind) }), _jsx(TableCell, { children: inboxStateText(group.state).text }), _jsx(TableCell, { numeric: true, className: "op-mono", children: groupDigits(String(group.count)) })] }, `count:${index}`))) })] }) })), unreadableGroups === 0 ? null : (_jsxs("p", { className: "op-hint", children: [groupDigits(String(unreadableGroups)), " count row", unreadableGroups === 1 ? " could" : "s could", " not be read, and ", unreadableGroups === 1 ? "is" : "are", " not in the table above."] }))] }));
28
+ }
29
+ /** One list of events: a channel account's, or the unmatched ones. Each keeps its own selection. */
30
+ function EventSection({ source, rev, scopeId, headingId, title, caption, emptyText, }) {
31
+ const listing = useAsync(() => source.listInbox(scopeId), [source, rev, scopeId]);
32
+ const [picked, setPicked] = useState(null);
33
+ if (listing.state.status === "error") {
34
+ return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, className: "op-section-title", children: title }), _jsx(LoadError, { message: "Could not read these events from the daemon.", onRetry: listing.reload })] }));
35
+ }
36
+ if (listing.state.status === "loading") {
37
+ return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, className: "op-section-title", children: title }), _jsx(SkeletonRows, { label: "Loading the events", rows: 3 })] }));
38
+ }
39
+ const { rows, moreAvailable } = listing.state.value;
40
+ if (rows.length === 0) {
41
+ return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, className: "op-section-title", children: title }), _jsx(EmptyState, { title: "No events yet", children: _jsx("p", { children: emptyText }) })] }));
42
+ }
43
+ const selected = rows.find((row) => row.readable === true && row.eventId === picked) ?? null;
44
+ return (_jsxs("section", { "aria-labelledby": headingId, children: [_jsxs("h2", { id: headingId, className: "op-section-title", children: [title, " (", groupDigits(String(rows.length)), ")"] }), moreAvailable ? (_jsx("p", { className: "op-hint", children: "Only the newest page is loaded here. Older events exist and are not shown on this screen." })) : null, _jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": headingId, children: [_jsx(TableCaption, { children: caption }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Received (UTC)" }), _jsx(TableHeaderCell, { children: "Platform" }), _jsx(TableHeaderCell, { children: "Kind" }), _jsx(TableHeaderCell, { children: "Sender id, as given" }), _jsx(TableHeaderCell, { children: "Text, as received" }), _jsx(TableHeaderCell, { children: "Event time, as given" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Marks" }), _jsx(TableHeaderCell, { children: "Raw item" })] }) }), _jsx(TableBody, { children: rows.map((row, index) => row.readable === true ? (_jsx(EventRow, { event: row, selected: selected?.eventId === row.eventId, onSelect: () => setPicked(row.eventId) }, `event:${row.eventId}`)) : (_jsx(UnreadableTableRow, { row: row }, `unreadable:${index}`))) })] }) }), _jsx("p", { role: "status", "aria-live": "polite", className: "op-hint", children: selected === null ? "" : `Showing the raw item of event ${selected.eventId}.` }), selected === null ? null : (_jsx(RawItem, { source: source, rev: rev, scopeId: scopeId, eventId: selected.eventId, idPrefix: headingId }, selected.eventId))] }));
45
+ }
46
+ function EventRow({ event, selected, onSelect }) {
47
+ const state = inboxStateText(event.state);
48
+ return (_jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, className: "op-mono", children: instantLabel(event.receivedAtUtc, "UTC") }), _jsx(TableCell, { children: platformName(event.platform) }), _jsx(TableCell, { children: inboxKindText(event.kind) }), _jsx(TableCell, { className: "op-mono", children: event.senderId ?? "No sender id given" }), _jsx(TableCell, { children: event.text === null ? "No text in this event" : _jsx("span", { className: "op-inbox__text", children: event.text }) }), _jsx(TableCell, { className: "op-mono", children: event.eventTimeAsGiven ?? "Not given" }), _jsxs(TableCell, { children: [_jsx(Badge, { tone: state.tone, children: state.text }), event.stateBy !== null && event.stateAtUtc !== null ? (_jsxs("div", { children: ["Set by ", event.stateBy, " at ", instantLabel(event.stateAtUtc, "UTC")] })) : null, event.stateNote === "" ? null : _jsxs("div", { children: ["Note: ", event.stateNote] })] }), _jsxs(TableCell, { children: [event.unmatched ? (_jsxs("p", { children: [_jsx("span", { className: "op-mark", children: "Not matched" }), " ", unmatchedText(event.unmatchedReason)] })) : null, event.oversize ? (_jsxs("p", { children: [_jsx("span", { className: "op-mark", children: "Cut" }), " A field was cut to its bound. The raw item keeps all of it."] })) : null, event.unmatched || event.oversize ? null : _jsx("p", { children: "None" })] }), _jsx(TableCell, { children: _jsx(Button, { size: "sm", variant: "secondary", "aria-pressed": selected, "aria-label": `Show the raw item of event ${event.eventId}`, onClick: onSelect, children: selected ? "Showing" : "Show" }) })] }));
49
+ }
50
+ /** A row the desktop cannot read: shown with its reason and its raw JSON, never dropped. */
51
+ function UnreadableTableRow({ row }) {
52
+ return (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: 9, children: [_jsxs("p", { children: ["This row could not be read: ", row.reason, ". It is kept here with its raw JSON."] }), _jsx("pre", { className: "op-raw", tabIndex: 0, "aria-label": "Raw JSON of a row that could not be read", children: row.rawJson })] }) }));
53
+ }
54
+ /** The raw item of the selected event, as the daemon kept it: untrusted text in a read-only, wrapping block. */
55
+ function RawItem({ source, rev, scopeId, eventId, idPrefix, }) {
56
+ const raw = useAsync(() => source.getInboxEvent(scopeId, eventId), [source, rev, scopeId, eventId]);
57
+ const titleId = `${idPrefix}-raw`;
58
+ if (raw.state.status === "error") {
59
+ return _jsx(LoadError, { message: "Could not read the raw item from the daemon.", onRetry: raw.reload });
60
+ }
61
+ if (raw.state.status === "loading") {
62
+ return _jsx(SkeletonRows, { label: "Loading the raw item", rows: 2 });
63
+ }
64
+ const item = raw.state.value;
65
+ if (item === null)
66
+ return _jsx("p", { className: "op-hint", children: "No raw item is stored for this event in this channel." });
67
+ return (_jsxs("section", { "aria-labelledby": titleId, children: [_jsx("h3", { id: titleId, className: "op-section-title", children: "Raw item, as received" }), _jsx("p", { className: "op-hint", children: "Written by the sender. Shown as plain text in a read-only block. Nothing in it is a link or markup." }), _jsx("pre", { className: "op-raw", tabIndex: 0, "aria-labelledby": titleId, children: item.rawJson })] }));
68
+ }
69
+ function unmatchedText(reason) {
70
+ if (reason === "no_account")
71
+ return "No stored account has this recipient id.";
72
+ if (reason === "ambiguous_account") {
73
+ return "More than one stored account has this recipient id, so none was chosen.";
74
+ }
75
+ return reason === null ? "Reason not stated." : `Reason as stored: ${reason}`;
76
+ }
@@ -0,0 +1,7 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ }
6
+ export declare function LinksScreen({ source, rev }: Props): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,52 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Button, EmptyState, PageShell, SkeletonRows, Table, TableBody, TableCaption, TableCell, TableHead, TableHeaderCell, TableRow } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { shell } from "../lib/shell.js";
8
+ import { groupDigits, instantLabel } from "../data/index.js";
9
+ import { platformName } from "./labels.js";
10
+ import { hostAppName } from "../appName.js";
11
+ export function LinksScreen({ source, rev }) {
12
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
13
+ const [picked, setPicked] = useState(null);
14
+ const scopeList = scopes.value ?? [];
15
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
16
+ return (_jsxs(PageShell, { title: "Links", subtitle: "Read only. The tracked links of one channel: each destination, its UTM parts and full tagged URL, the campaign and the post it belongs to, and the visit counts the daemon holds. Nothing here cuts a link, records a visit or redirects a visitor.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "Links are kept per channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => setPicked(id) }), _jsx(LinksView, { source: source, rev: rev, scopeId: scopeId }, scopeId)] }))] }));
17
+ }
18
+ function LinksView({ source, rev, scopeId }) {
19
+ const links = useAsync(() => source.listChannelLinks(scopeId), [source, rev, scopeId]);
20
+ const [notice, setNotice] = useState("");
21
+ const copy = async (text, what) => {
22
+ try {
23
+ await shell.clipboard.write({ text });
24
+ setNotice(`Copied ${what}.`);
25
+ }
26
+ catch {
27
+ setNotice(`Copying ${what} failed. Select the text and copy it by hand.`);
28
+ }
29
+ };
30
+ if (links.state.status === "error") {
31
+ return _jsx(LoadError, { message: "Could not read the tracked links from the daemon.", onRetry: links.reload });
32
+ }
33
+ if (links.value === null)
34
+ return _jsx(SkeletonRows, { label: "Loading the tracked links", rows: 3 });
35
+ const rows = links.value;
36
+ if (rows.length === 0) {
37
+ return (_jsx(EmptyState, { title: "No links yet", children: _jsx("p", { children: "Nothing is wrong: no links have been recorded for this channel yet. Links are cut on the daemon host, not here." }) }));
38
+ }
39
+ return (_jsxs(_Fragment, { children: [_jsxs("p", { className: "op-hint", children: [hostAppName(), " records links; it does not count clicks here. A link is a string: nothing serves it or redirects a visitor. Visit counts are the plain numbers the daemon has recorded, shown exactly as stored. The engine gives a count no time and no source of its own, so each row says so. Nothing here computes a rate, a total or a ranking."] }), _jsx("p", { role: "status", "aria-live": "polite", className: "op-hint", children: notice }), _jsxs("section", { "aria-labelledby": "links-list-title", children: [_jsxs("h2", { id: "links-list-title", className: "op-section-title", children: ["Tracked links (", groupDigits(String(rows.length)), ")"] }), _jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "links-list-title", children: [_jsx(TableCaption, { children: "Tracked links in this channel, by campaign, variant, platform and link id" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Link" }), _jsx(TableHeaderCell, { children: "Campaign" }), _jsx(TableHeaderCell, { children: "Post (calendar entry)" }), _jsx(TableHeaderCell, { children: "Destination" }), _jsx(TableHeaderCell, { children: "UTM parts" }), _jsx(TableHeaderCell, { children: "Full tagged URL" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Created (UTC)" }), _jsx(TableHeaderCell, { children: "Last changed" }), _jsx(TableHeaderCell, { align: "end", children: "Visits recorded" })] }) }), _jsx(TableBody, { children: rows.map((row) => (_jsx(LinkRow, { row: row, copy: copy }, `link:${row.link.scopeId}:${row.link.id}`))) })] }) })] })] }));
40
+ }
41
+ /** One link. Each URL is text with its own Copy button; neither is an anchor, so nothing opens from here. */
42
+ function LinkRow({ row, copy }) {
43
+ const { link } = row;
44
+ const label = `${platformName(link.platform)}, variant ${link.variantId}, link ${link.id}`;
45
+ const utmParts = [
46
+ ["utm_source", link.utmSource],
47
+ ["utm_medium", link.utmMedium],
48
+ ["utm_campaign", link.utmCampaign],
49
+ ["utm_content", link.utmContent],
50
+ ];
51
+ return (_jsxs(TableRow, { children: [_jsxs(TableCell, { rowHeader: true, children: [_jsx("div", { className: "op-mono", children: link.id }), _jsxs("div", { children: [platformName(link.platform), ", variant ", _jsx("span", { className: "op-mono", children: link.variantId })] })] }), _jsxs(TableCell, { children: [_jsx("div", { children: row.campaignName }), _jsx("div", { className: "op-mono", children: link.campaignId })] }), _jsx(TableCell, { className: "op-mono", children: link.calendarEntryId ?? "No calendar entry attached" }), _jsxs(TableCell, { children: [_jsx("div", { className: "op-mono", children: link.destination }), _jsx(Button, { size: "sm", variant: "secondary", "aria-label": `Copy the destination of ${label}`, onClick: () => copy(link.destination, `the destination of ${label}`), children: "Copy" })] }), _jsx(TableCell, { children: _jsx("ul", { className: "op-id-list", children: utmParts.map(([name, value]) => (_jsxs("li", { className: "op-mono", children: [name, ": ", value] }, name))) }) }), _jsxs(TableCell, { children: [_jsx("div", { className: "op-mono", children: link.finalUrl }), _jsx(Button, { size: "sm", variant: "secondary", "aria-label": `Copy the full tagged URL of ${label}`, onClick: () => copy(link.finalUrl, `the full tagged URL of ${label}`), children: "Copy" })] }), _jsx(TableCell, { children: "Stored. Links are never edited, so there is no later state." }), _jsx(TableCell, { className: "op-mono", children: instantLabel(link.createdAtUtc, "UTC") }), _jsx(TableCell, { children: "Never: a link is not edited after it is created." }), _jsxs(TableCell, { numeric: true, children: [_jsx("span", { className: "op-mono", children: row.visits ?? "Not read" }), _jsx("div", { className: "op-hint", children: "Source: link.attribution, the recorded visit count. Time: not stated by the engine." })] })] }));
52
+ }
@@ -0,0 +1,7 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ }
6
+ export declare function PacingScreen({ source, rev }: Props): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,88 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, EmptyState, PageShell, SkeletonRows, Table, TableBody, TableCaption, TableCell, TableHead, TableHeaderCell, TableRow } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { groupDigits, instantLabel, } from "../data/index.js";
8
+ import { PACING_REASON_LABEL, PUBLISH_STATE_LABEL, platformName } from "./labels.js";
9
+ import { hostAppName } from "../appName.js";
10
+ export function PacingScreen({ source, rev }) {
11
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
12
+ const [picked, setPicked] = useState(null);
13
+ const scopeList = scopes.value ?? [];
14
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
15
+ return (_jsxs(PageShell, { title: "Pacing", subtitle: "Read only. What the daemon's publish pacing lets run now on one channel, every job that may not run with its reason and the earliest time it could, and each account's pacing as stored. Nothing here queues, runs, pauses or changes a limit.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "Publish pacing is kept per channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => setPicked(id) }), _jsxs("p", { className: "op-hint", children: ["The default gap between posts and the default count per UTC day are ", hostAppName(), "'s own caution. They are not platform limits. A limit is set on the server by a person, not here."] }), _jsx(PacingView, { source: source, rev: rev, scopeId: scopeId, scopes: scopeList }, scopeId)] }))] }));
16
+ }
17
+ function PacingView({ source, rev, scopeId, scopes, }) {
18
+ const view = useAsync(() => source.listPublishNext(scopeId), [source, rev, scopeId]);
19
+ if (view.state.status === "error") {
20
+ return _jsx(LoadError, { message: "Could not read the publish pacing from the daemon.", onRetry: view.reload });
21
+ }
22
+ if (view.state.status === "loading") {
23
+ return _jsx(SkeletonRows, { label: "Loading the publish pacing", rows: 3 });
24
+ }
25
+ const data = view.state.value;
26
+ const total = data.ready.length + data.blocked.length + data.inProgress.length;
27
+ if (total === 0) {
28
+ return (_jsx(EmptyState, { title: "Nothing is queued", children: _jsx("p", { children: "Nothing is queued for this channel." }) }));
29
+ }
30
+ return (_jsxs(_Fragment, { children: [_jsxs("p", { role: "status", "aria-live": "polite", className: "op-hint", children: ["Decided at ", instantLabel(data.nowUtc, "UTC"), " by the daemon's clock."] }), _jsxs("section", { "aria-labelledby": "pacing-ready-title", children: [_jsxs("h2", { id: "pacing-ready-title", className: "op-section-title", children: ["May run now (", groupDigits(String(data.readyTotal)), ")"] }), data.ready.length === 0 ? (_jsx("p", { className: "op-hint", children: "No job may run now on this channel." })) : (_jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "pacing-ready-title", children: [_jsx(TableCaption, { children: "Jobs that may run now, at most one per account, earliest scheduled time first" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Platform and remote account" }), _jsx(TableHeaderCell, { children: "Channel account" }), _jsx(TableHeaderCell, { children: "Job" }), _jsx(TableHeaderCell, { children: "Calendar entry" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Phase" }), _jsx(TableHeaderCell, { children: "Scheduled (UTC)" })] }) }), _jsx(TableBody, { children: data.ready.map((row, index) => row.readable === true ? (_jsx(JobRow, { job: row, scopes: scopes }, `ready:${row.jobId}`)) : (_jsx(UnreadableJobRow, { row: row, colSpan: 7 }, `ready-unreadable:${index}`))) })] }) }))] }), _jsxs("section", { "aria-labelledby": "pacing-blocked-title", children: [_jsxs("h2", { id: "pacing-blocked-title", className: "op-section-title", children: ["May not run now (", groupDigits(String(data.blocked.length)), ")"] }), _jsx("p", { className: "op-hint", children: "Where a job waits behind another job on its account, the earliest time shown is a lower bound, not a promise. A job with no time given needs a person or a check first." }), data.blocked.length === 0 ? (_jsx("p", { className: "op-hint", children: "No job is held back." })) : (_jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "pacing-blocked-title", children: [_jsx(TableCaption, { children: "Jobs that may not run now, with the reason in words, the other reasons, and the earliest run time" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Platform and remote account" }), _jsx(TableHeaderCell, { children: "Channel account" }), _jsx(TableHeaderCell, { children: "Job" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Reason" }), _jsx(TableHeaderCell, { children: "Other reasons that apply" }), _jsx(TableHeaderCell, { children: "Earliest run (UTC)" }), _jsx(TableHeaderCell, { children: "Detail, as given" }), _jsx(TableHeaderCell, { children: "Behind job" })] }) }), _jsx(TableBody, { children: data.blocked.map((row, index) => row.readable === true ? (_jsx(BlockedRow, { job: row, scopes: scopes }, `blocked:${row.jobId}`)) : (_jsx(UnreadableJobRow, { row: row, colSpan: 9 }, `blocked-unreadable:${index}`))) })] }) }))] }), _jsxs("section", { "aria-labelledby": "pacing-progress-title", children: [_jsxs("h2", { id: "pacing-progress-title", className: "op-section-title", children: ["In progress (", groupDigits(String(data.inProgress.length)), ")"] }), _jsx("p", { className: "op-hint", children: "A job already past its create is in progress. Pacing does not hold it back; the daemon continues it." }), data.inProgress.length === 0 ? (_jsx("p", { className: "op-hint", children: "No job is in progress on this channel." })) : (_jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": "pacing-progress-title", children: [_jsx(TableCaption, { children: "Jobs in progress on this channel" }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Platform and remote account" }), _jsx(TableHeaderCell, { children: "Channel account" }), _jsx(TableHeaderCell, { children: "Job" }), _jsx(TableHeaderCell, { children: "Calendar entry" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Phase" }), _jsx(TableHeaderCell, { children: "Scheduled (UTC)" })] }) }), _jsx(TableBody, { children: data.inProgress.map((row, index) => row.readable === true ? (_jsx(JobRow, { job: row, scopes: scopes }, `progress:${row.jobId}`)) : (_jsx(UnreadableJobRow, { row: row, colSpan: 7 }, `progress-unreadable:${index}`))) })] }) }))] }), _jsx(AccountsSection, { accounts: accountsOf(data), source: source, rev: rev, scopes: scopes, nowUtc: data.nowUtc })] }));
31
+ }
32
+ function scopeLabelOf(scopes, scopeId) {
33
+ return scopes.find((s) => s.id === scopeId)?.label ?? "Not in the channel list";
34
+ }
35
+ function JobRow({ job, scopes }) {
36
+ const state = PUBLISH_STATE_LABEL[job.state];
37
+ return (_jsxs(TableRow, { children: [_jsxs(TableCell, { rowHeader: true, children: [_jsx("div", { children: platformName(job.platform) }), _jsx("div", { className: "op-mono", children: job.remoteAccount })] }), _jsx(TableCell, { children: scopeLabelOf(scopes, job.scopeId) }), _jsx(TableCell, { className: "op-mono", children: job.jobId }), _jsx(TableCell, { className: "op-mono", children: job.entryId }), _jsx(TableCell, { children: _jsx(Badge, { tone: state.tone, children: state.text }) }), _jsx(TableCell, { className: "op-mono", children: job.phase }), _jsx(TableCell, { className: "op-mono", children: instantLabel(job.scheduledAtUtc, "UTC") })] }));
38
+ }
39
+ function BlockedRow({ job, scopes }) {
40
+ const state = PUBLISH_STATE_LABEL[job.state];
41
+ const others = job.reasons.filter((reason) => reason !== job.reason);
42
+ return (_jsxs(TableRow, { children: [_jsxs(TableCell, { rowHeader: true, children: [_jsx("div", { children: platformName(job.platform) }), _jsx("div", { className: "op-mono", children: job.remoteAccount })] }), _jsx(TableCell, { children: scopeLabelOf(scopes, job.scopeId) }), _jsx(TableCell, { className: "op-mono", children: job.jobId }), _jsx(TableCell, { children: _jsx(Badge, { tone: state.tone, children: state.text }) }), _jsx(TableCell, { children: PACING_REASON_LABEL[job.reason] }), _jsx(TableCell, { children: others.length === 0 ? "None" : others.map((reason) => PACING_REASON_LABEL[reason]).join("; ") }), _jsx(TableCell, { className: "op-mono", children: job.untilUtc === null ? "Not given" : instantLabel(job.untilUtc, "UTC") }), _jsx(TableCell, { children: job.detail }), _jsx(TableCell, { className: "op-mono", children: job.behindJobId ?? "None" })] }));
43
+ }
44
+ /** A row the desktop cannot read: shown with its reason and its raw JSON, never dropped. */
45
+ function UnreadableJobRow({ row, colSpan }) {
46
+ return (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: colSpan, children: [_jsxs("p", { children: ["This row could not be read: ", row.reason, ". It is kept here with its raw JSON."] }), _jsx("pre", { className: "op-raw", tabIndex: 0, "aria-label": "Raw JSON of a row that could not be read", children: row.rawJson })] }) }));
47
+ }
48
+ /** Each distinct account the jobs name, once, in the order the daemon lists its jobs. */
49
+ function accountsOf(view) {
50
+ const rows = [...view.ready, ...view.inProgress, ...view.blocked];
51
+ const accounts = new Map();
52
+ for (const row of rows) {
53
+ if (row.readable !== true)
54
+ continue;
55
+ const id = JSON.stringify([row.scopeId, row.platform, row.remoteAccount]);
56
+ if (!accounts.has(id)) {
57
+ accounts.set(id, {
58
+ key: row.jobId,
59
+ scopeId: row.scopeId,
60
+ platform: row.platform,
61
+ remoteAccount: row.remoteAccount,
62
+ });
63
+ }
64
+ }
65
+ return [...accounts.values()];
66
+ }
67
+ function AccountsSection({ accounts, source, rev, scopes, nowUtc, }) {
68
+ return (_jsxs("section", { "aria-labelledby": "pacing-accounts-title", children: [_jsxs("h2", { id: "pacing-accounts-title", className: "op-section-title", children: ["Pacing per account (", groupDigits(String(accounts.length)), ")"] }), _jsxs("p", { className: "op-hint", children: ["Each account shows the limits in force. \"Defaults\" means no limit was stored for it, so ", hostAppName(), "'s own caution applies. \"Stored\" means a person set it on the server. This screen changes nothing."] }), _jsx("p", { className: "op-hint", children: "An account with no queued, waiting or running job is not listed here. The daemon names an account's remote id only in its jobs." }), accounts.length === 0 ? _jsx("p", { className: "op-hint", children: "No account has a job to show pacing for." }) : null, accounts.map((account) => (_jsx(AccountPacing, { source: source, rev: rev, account: account, scopes: scopes, nowUtc: nowUtc }, account.key)))] }));
69
+ }
70
+ /** One account's pacing. Its own read: a failed read shows for this account only, and the other accounts still show. */
71
+ function AccountPacing({ source, rev, account, scopes, nowUtc, }) {
72
+ const quota = useAsync(() => source.getPublishQuota(account.scopeId, account.platform, account.remoteAccount), [source, rev, account.scopeId, account.platform, account.remoteAccount]);
73
+ const name = `${platformName(account.platform)} account ${account.remoteAccount}`;
74
+ if (quota.state.status === "error") {
75
+ return _jsx(LoadError, { message: `Could not read the pacing of the ${name} from the daemon.`, onRetry: quota.reload });
76
+ }
77
+ if (quota.state.status === "loading") {
78
+ return _jsx(SkeletonRows, { label: `Loading the pacing of the ${name}`, rows: 2 });
79
+ }
80
+ const q = quota.state.value;
81
+ const now = Date.parse(nowUtc);
82
+ const limitsText = q.optedIn ? "Stored by a person on the server" : `${hostAppName()} defaults: no limit stored`;
83
+ return (_jsxs("div", { children: [_jsxs("p", { className: "op-hint", children: ["Channel account: ", scopeLabelOf(scopes, account.scopeId)] }), _jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-label": `Pacing of the ${name}, as stored`, children: [_jsxs(TableCaption, { children: ["Pacing of the ", name, ", as stored"] }), _jsxs(TableBody, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Limits in force" }), _jsx(TableCell, { children: limitsText })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Gap between two creates" }), _jsxs(TableCell, { className: "op-mono", children: [q.minGapSecs, " seconds", q.optedIn ? `. ${hostAppName()} default: ${q.defaultMinGapSecs} seconds.` : " (the default)"] })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Creates per UTC day" }), _jsxs(TableCell, { className: "op-mono", children: [q.maxPerDay, q.optedIn ? `. ${hostAppName()} default: ${q.defaultMaxPerDay}.` : " (the default)"] })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Paused until" }), _jsx(TableCell, { children: q.pausedUntilUtc === null
84
+ ? "Not paused"
85
+ : `${instantLabel(q.pausedUntilUtc, "UTC")}, ${Date.parse(q.pausedUntilUtc) > now ? "in force" : "ended"}` })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Last create (UTC)" }), _jsx(TableCell, { className: "op-mono", children: q.lastCreateAtUtc === null ? "None recorded" : instantLabel(q.lastCreateAtUtc, "UTC") })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Creates today (UTC day)" }), _jsxs(TableCell, { className: "op-mono", children: [q.createsToday, ". The count restarts ", instantLabel(q.dayResetsAtUtc, "UTC"), "."] })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { rowHeader: true, children: "Platform wait" }), _jsxs(TableCell, { children: [q.rateLimitedUntilUtc === null
86
+ ? "None recorded"
87
+ : `Until ${instantLabel(q.rateLimitedUntilUtc, "UTC")}, ${Date.parse(q.rateLimitedUntilUtc) > now ? "in force" : "ended"}`, q.rateLimitedAtUtc === null ? null : ` The signal was seen ${instantLabel(q.rateLimitedAtUtc, "UTC")}.`] })] })] })] }) })] }));
88
+ }
@@ -0,0 +1,7 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ }
6
+ export declare function PublishJobsScreen({ source, rev }: Props): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,66 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, Button, EmptyState, PageShell, SkeletonRows, Table, TableBody, TableCaption, TableCell, TableHead, TableHeaderCell, TableRow } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { shell } from "../lib/shell.js";
8
+ import { groupDigits, instantLabel, } from "../data/index.js";
9
+ import { PUBLISH_RESOLUTION_LABEL, PUBLISH_STATE_LABEL, platformName } from "./labels.js";
10
+ import { hostAppName } from "../appName.js";
11
+ /** The plain explanation shown above every outcome_unknown job. The wording is fixed. */
12
+ const OUTCOME_UNKNOWN_TEXT = `The platform may or may not have received the post. ${hostAppName()} will not send it again by itself. A person must look at the account on the platform, then record what they saw with the publish.resolve_unknown command on the server. This screen cannot change a job.`;
13
+ export function PublishJobsScreen({ source, rev }) {
14
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
15
+ const [picked, setPicked] = useState(null);
16
+ const scopeList = scopes.value ?? [];
17
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
18
+ return (_jsxs(PageShell, { title: "Publish jobs", subtitle: "Read only. Every publish job of one channel, newest first: its platform, calendar entry, state, phase, attempts, next attempt, remote post and last error. Jobs whose outcome is unknown are listed first. Nothing here queues, steps, checks, cancels or resolves a job.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "Publish jobs are kept per channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => setPicked(id) }), _jsx(PublishJobsView, { source: source, rev: rev, scopeId: scopeId }, scopeId)] }))] }));
19
+ }
20
+ function PublishJobsView({ source, rev, scopeId }) {
21
+ const jobs = useAsync(() => source.listPublishJobs(scopeId), [source, rev, scopeId]);
22
+ const [notice, setNotice] = useState("");
23
+ const copy = async (text, what) => {
24
+ try {
25
+ await shell.clipboard.write({ text });
26
+ setNotice(`Copied ${what}.`);
27
+ }
28
+ catch {
29
+ setNotice(`Copying ${what} failed. Select the text and copy it by hand.`);
30
+ }
31
+ };
32
+ if (jobs.state.status === "error") {
33
+ return _jsx(LoadError, { message: "Could not read the publish jobs from the daemon.", onRetry: jobs.reload });
34
+ }
35
+ if (jobs.state.status === "loading") {
36
+ return _jsx(SkeletonRows, { label: "Loading the publish jobs", rows: 3 });
37
+ }
38
+ const rows = jobs.state.value;
39
+ if (rows.length === 0) {
40
+ return (_jsx(EmptyState, { title: "No publish jobs yet", children: _jsx("p", { children: "Nothing has been queued for publishing on this channel yet." }) }));
41
+ }
42
+ const unknown = rows.filter((job) => job.state === "outcome_unknown");
43
+ const others = rows.filter((job) => job.state !== "outcome_unknown");
44
+ return (_jsxs(_Fragment, { children: [_jsx("p", { className: "op-hint", children: "The daemon publishes; this screen shows only what it stored. Each value is shown exactly as stored. Nothing is sent to a platform from here." }), _jsx("p", { role: "status", "aria-live": "polite", className: "op-hint", children: notice }), _jsxs("section", { "aria-labelledby": "publish-unknown-title", children: [_jsxs("h2", { id: "publish-unknown-title", className: "op-section-title", children: ["Outcome unknown (", groupDigits(String(unknown.length)), ")"] }), _jsx("p", { className: "op-hint", children: OUTCOME_UNKNOWN_TEXT }), unknown.length === 0 ? (_jsx("p", { className: "op-hint", children: "No job has an unknown outcome." })) : (_jsx(JobTable, { caption: "Publish jobs whose outcome is unknown, newest first", labelledBy: "publish-unknown-title", rows: unknown, copy: copy }))] }), _jsxs("section", { "aria-labelledby": "publish-other-title", children: [_jsxs("h2", { id: "publish-other-title", className: "op-section-title", children: ["Other publish jobs (", groupDigits(String(others.length)), ")"] }), others.length === 0 ? (_jsx("p", { className: "op-hint", children: "No other publish job." })) : (_jsx(JobTable, { caption: "Publish jobs of this channel, newest first", labelledBy: "publish-other-title", rows: others, copy: copy }))] })] }));
45
+ }
46
+ function JobTable({ caption, labelledBy, rows, copy, }) {
47
+ return (_jsx("div", { className: "op-table-scroll", children: _jsxs(Table, { "aria-labelledby": labelledBy, children: [_jsx(TableCaption, { children: caption }), _jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Job and platform" }), _jsx(TableHeaderCell, { children: "Calendar entry" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Phase" }), _jsx(TableHeaderCell, { align: "end", children: "Attempts" }), _jsx(TableHeaderCell, { children: "Next attempt (UTC)" }), _jsx(TableHeaderCell, { children: "Remote post and link" }), _jsx(TableHeaderCell, { children: "Last error" }), _jsx(TableHeaderCell, { children: "Resolved by a person" })] }) }), _jsx(TableBody, { children: rows.map((job) => (_jsx(JobRow, { job: job, copy: copy }, `job:${job.jobId}`))) })] }) }));
48
+ }
49
+ function JobRow({ job, copy }) {
50
+ const state = PUBLISH_STATE_LABEL[job.state];
51
+ const what = `${platformName(job.platform)} job ${job.jobId}`;
52
+ const link = job.permalink;
53
+ return (_jsxs(TableRow, { children: [_jsxs(TableCell, { rowHeader: true, children: [_jsx("div", { children: platformName(job.platform) }), _jsx("div", { className: "op-mono", children: job.jobId })] }), _jsxs(TableCell, { children: [_jsx("div", { className: "op-mono", children: job.entryId }), _jsxs("div", { children: ["Entry revision ", job.entryRevision] })] }), _jsx(TableCell, { children: _jsx(Badge, { tone: state.tone, children: state.text }) }), _jsx(TableCell, { className: "op-mono", children: job.phase }), _jsx(TableCell, { numeric: true, className: "op-mono", children: job.attempts }), _jsx(TableCell, { className: "op-mono", children: nextAttemptText(job) }), _jsxs(TableCell, { children: [job.remotePostId === null ? (_jsx("div", { children: "No remote post id." })) : (_jsxs("div", { children: ["Post id: ", _jsx("span", { className: "op-mono", children: job.remotePostId })] })), link === null ? (_jsx("div", { children: "No platform link." })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "op-mono", children: link }), _jsx(Button, { size: "sm", variant: "secondary", "aria-label": `Copy the permalink of ${what}`, onClick: () => copy(link, `the permalink of ${what}`), children: "Copy" })] }))] }), _jsx(TableCell, { children: job.errorCode === null ? ("No error recorded") : (_jsxs(_Fragment, { children: [_jsx("div", { className: "op-mono", children: job.errorCode }), _jsx("div", { children: job.errorMessage ?? "No message given." })] })) }), _jsx(TableCell, { children: job.resolution === null ? "Not resolved by a person" : _jsx(ResolutionView, { resolution: job.resolution }) })] }));
54
+ }
55
+ /** Who resolved an unknown job, when, what they decided, their note, and whether the remote id was typed by hand. */
56
+ function ResolutionView({ resolution }) {
57
+ return (_jsxs("div", { children: [_jsxs("div", { children: ["Recorded by ", resolution.resolvedBy, " at ", instantLabel(resolution.resolvedAtUtc, "UTC")] }), _jsxs("div", { children: ["Decision: ", PUBLISH_RESOLUTION_LABEL[resolution.decision]] }), _jsxs("div", { children: ["Note: ", resolution.note] }), resolution.remoteIdEnteredByHand ? (_jsxs("div", { children: [_jsx("span", { className: "op-mark", children: "Entered by hand" }), " The remote id was typed by a person. The platform did not return it."] })) : null] }));
58
+ }
59
+ /** The next attempt only while the job waits. Any other state says it is not waiting. */
60
+ function nextAttemptText(job) {
61
+ if (job.state !== "waiting")
62
+ return "Not waiting";
63
+ return job.nextAttemptAtUtc === null
64
+ ? "Waiting. The next attempt time is not reported."
65
+ : instantLabel(job.nextAttemptAtUtc, "UTC");
66
+ }
@@ -0,0 +1,8 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ onChanged: () => void;
6
+ }
7
+ export declare function SuggestionsScreen({ source, rev, onChanged }: Props): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,94 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Badge, Button, EmptyState, Notice, PageShell, SkeletonRows } from "@rightkit/react-shell/react";
4
+ import { ScopeSelect } from "../components/ScopeSelect.js";
5
+ import { LoadError, MockBanner } from "../components/Shared.js";
6
+ import { useAsync } from "../hooks/useAsync.js";
7
+ import { groupDigits, instantLabel, } from "../data/index.js";
8
+ import { OPTIMISE_STATE_LABEL, platformName } from "./labels.js";
9
+ const SECTIONS = [
10
+ { state: "proposed", title: "Waiting for your decision", empty: "No suggestion is waiting for a decision." },
11
+ { state: "accepted", title: "Accepted", empty: "No suggestion has been accepted." },
12
+ { state: "dismissed", title: "Dismissed", empty: "No suggestion has been dismissed." },
13
+ ];
14
+ /** Identifiers in the engine contain characters DOM ids do not allow here; keep letters, digits, "_" and "-". */
15
+ function domId(raw) {
16
+ return raw.replace(/[^A-Za-z0-9_-]/g, "_");
17
+ }
18
+ export function SuggestionsScreen({ source, rev, onChanged }) {
19
+ const scopes = useAsync(() => source.listScopes(), [source, rev]);
20
+ const [picked, setPicked] = useState(null);
21
+ const [notice, setNotice] = useState(null);
22
+ const scopeList = scopes.value ?? [];
23
+ const scopeId = scopeList.find((s) => s.id === picked)?.id ?? scopeList[0]?.id ?? null;
24
+ return (_jsxs(PageShell, { title: "Suggestions", subtitle: "Calendar changes proposed from stored analytics. Accepting one creates a draft calendar entry. Nothing is approved or published.", children: [source.kind === "mock" ? _jsx(MockBanner, {}) : null, _jsx("p", { className: "op-hint", children: "A draft entry describes the suggestion. Write its real caption on the calendar, then approve it there." }), scopes.state.status === "error" ? (_jsx(LoadError, { message: "Could not read the channels from the daemon.", onRetry: scopes.reload })) : scopes.value === null ? (_jsx(SkeletonRows, { label: "Loading channels", rows: 2 })) : scopeList.length === 0 || scopeId === null ? (_jsx(EmptyState, { title: "No channels yet", children: _jsx("p", { children: "Suggestions appear for a channel once one is set up." }) })) : (_jsxs(_Fragment, { children: [_jsx(ScopeSelect, { scopes: scopeList, value: scopeId, onChange: (id) => {
25
+ setPicked(id);
26
+ setNotice(null);
27
+ } }), notice ? (_jsx(Notice, { tone: notice.tone, className: "op-notice", children: notice.text })) : null, _jsx(ProposalList, { source: source, rev: rev, scopeId: scopeId, onChanged: onChanged, onNotice: setNotice }, scopeId)] }))] }));
28
+ }
29
+ function ProposalList({ source, rev, scopeId, onChanged, onNotice, }) {
30
+ const { state, reload, value } = useAsync(() => source.listProposals(scopeId), [source, rev, scopeId]);
31
+ if (state.status === "error") {
32
+ return _jsx(LoadError, { message: "Could not read suggestions from the daemon.", onRetry: reload });
33
+ }
34
+ if (value === null)
35
+ return _jsx(SkeletonRows, { label: "Loading suggestions", rows: 3 });
36
+ if (value.length === 0) {
37
+ return (_jsx(EmptyState, { title: "Not enough evidence yet", children: _jsx("p", { children: "This channel has no suggestion yet. A suggestion needs more published posts with stored analytics before the rule can compare them. This is not an error." }) }));
38
+ }
39
+ return (_jsx(_Fragment, { children: SECTIONS.map((section) => {
40
+ const items = value.filter((proposal) => proposal.state === section.state);
41
+ return (_jsxs("section", { className: "op-drafts-section", "aria-labelledby": `suggestions-${section.state}`, children: [_jsxs("h2", { id: `suggestions-${section.state}`, className: "op-section-title", children: [section.title, " (", items.length, ")"] }), items.length === 0 ? (_jsx("p", { className: "op-hint", children: section.empty })) : (_jsx("div", { className: "op-drafts", children: items.map((proposal) => (_jsx(ProposalCard, { proposal: proposal, scopeId: scopeId, source: source, onChanged: onChanged, onNotice: onNotice }, proposal.id))) }))] }, section.state));
42
+ }) }));
43
+ }
44
+ function ProposalCard({ proposal, scopeId, source, onChanged, onNotice, }) {
45
+ const [busy, setBusy] = useState(false);
46
+ const [whyOpen, setWhyOpen] = useState(false);
47
+ const id = domId(proposal.id);
48
+ const badge = OPTIMISE_STATE_LABEL[proposal.state];
49
+ async function decide(action) {
50
+ setBusy(true);
51
+ const result = action === "accept"
52
+ ? await source.acceptProposal(scopeId, proposal.id, proposal.revision)
53
+ : await source.dismissProposal(scopeId, proposal.id, proposal.revision);
54
+ setBusy(false);
55
+ if (result.ok) {
56
+ onNotice(action === "accept"
57
+ ? {
58
+ tone: "ok",
59
+ text: `Draft calendar entry created: ${result.value.calendarEntryId ?? "(no id returned)"}. It still needs a real caption and approval before it can be scheduled or published.`,
60
+ }
61
+ : { tone: "ok", text: "Dismissed. No calendar entry was created." });
62
+ onChanged();
63
+ return;
64
+ }
65
+ if (result.error.code === "revision_conflict") {
66
+ onNotice({
67
+ tone: "bad",
68
+ text: "This suggestion changed after the list was read. Nothing was changed, and the list has been reloaded. Check it and decide again.",
69
+ });
70
+ onChanged();
71
+ return;
72
+ }
73
+ onNotice({ tone: "bad", text: result.error.message });
74
+ }
75
+ return (_jsxs("article", { className: "op-card op-draft", "aria-labelledby": `proposal-${id}`, children: [_jsxs("h3", { id: `proposal-${id}`, className: "op-draft__title", children: [platformName(proposal.platform), ": remake a top-decile post"] }), _jsx("div", { className: "op-badges", children: _jsx(Badge, { tone: badge.tone, children: badge.text }) }), _jsxs("dl", { className: "op-dl", children: [_jsx("dt", { children: "Rule" }), _jsx("dd", { children: "Remake a post that is in the top decile for its channel." }), _jsx("dt", { children: "Source post" }), _jsx("dd", { className: "op-mono", children: proposal.sourceJobId }), _jsx("dt", { children: "Metric and window" }), _jsxs("dd", { children: [proposal.metric, ", ", proposal.window] }), _jsx("dt", { children: "Value against threshold" }), _jsxs("dd", { children: [groupDigits(proposal.value), " against a threshold of ", groupDigits(proposal.threshold), ". A post wins when its value is at least the threshold."] }), _jsx("dt", { children: "Sample" }), _jsxs("dd", { children: [proposal.sampleSize, " published posts. The top decile holds ", proposal.decileSize, "."] }), _jsx("dt", { children: "Observed" }), _jsx("dd", { children: instantLabel(proposal.observedAtUtc, "UTC") }), _jsx("dt", { children: "Due" }), _jsx("dd", { children: instantLabel(proposal.dueAtUtc, "UTC") }), proposal.decidedAtUtc ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Decided" }), _jsxs("dd", { children: [instantLabel(proposal.decidedAtUtc, "UTC"), proposal.decisionNote ? `. Note: ${proposal.decisionNote}` : ""] })] })) : null, proposal.calendarEntryId ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Draft calendar entry" }), _jsx("dd", { className: "op-mono", children: proposal.calendarEntryId })] })) : null] }), _jsxs("details", { onToggle: (e) => setWhyOpen(e.currentTarget.open), children: [_jsx("summary", { children: "Why this was suggested" }), whyOpen ? _jsx(WhyDetail, { source: source, scopeId: scopeId, proposalId: proposal.id }) : null] }), proposal.state === "proposed" ? (_jsxs("div", { className: "op-decide__buttons", children: [_jsx(Button, { variant: "primary", disabled: busy, onClick: () => decide("accept"), children: "Accept" }), _jsx(Button, { variant: "secondary", disabled: busy, onClick: () => decide("dismiss"), children: "Dismiss" })] })) : null] }));
76
+ }
77
+ /** The lineage of one suggestion, read when its "why" opens. Loads once per open. */
78
+ function WhyDetail({ source, scopeId, proposalId }) {
79
+ const { state, reload } = useAsync(() => source.getProposal(scopeId, proposalId), [source, scopeId, proposalId]);
80
+ if (state.status === "error") {
81
+ return _jsx(LoadError, { message: "Could not read the lineage for this suggestion.", onRetry: reload });
82
+ }
83
+ if (state.status === "loading")
84
+ return _jsx("p", { className: "op-hint", children: "Loading the lineage." });
85
+ const detail = state.value;
86
+ if (detail === null)
87
+ return _jsx("p", { className: "op-hint", children: "This suggestion is no longer in this channel." });
88
+ return (_jsxs("dl", { className: "op-dl", children: [_jsx("dt", { children: "Published posts compared" }), _jsx("dd", { children: _jsx(IdList, { ids: detail.lineageJobIds }) }), _jsx("dt", { children: "Analytics pulls compared" }), _jsx("dd", { children: _jsx(IdList, { ids: detail.lineagePullIds }) })] }));
89
+ }
90
+ function IdList({ ids }) {
91
+ if (ids.length === 0)
92
+ return _jsx(_Fragment, { children: "None recorded." });
93
+ return (_jsx("ul", { className: "op-id-list", children: ids.map((id) => (_jsx("li", { className: "op-mono", children: id }, id))) }));
94
+ }
@@ -0,0 +1,7 @@
1
+ import { type DataSource } from "../data/index.js";
2
+ interface Props {
3
+ source: DataSource;
4
+ rev: number;
5
+ }
6
+ export declare function TextGateScreen({ source, rev }: Props): import("react").JSX.Element;
7
+ export {};