@odla-ai/chapter 0.20.2 → 0.22.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.
@@ -32,8 +32,50 @@ interface Rule {
32
32
  }
33
33
  /** Namespace → rule set. */
34
34
  type DbRules = Record<string, Rule>;
35
- /** Brand tokens that theme the site: palette, fonts, wordmark, nav, logos. */
35
+ /** Typed semantic roles shared by the public site and application surfaces. */
36
+ interface ChapterBrandTokens {
37
+ background?: string;
38
+ surface?: string;
39
+ surface2?: string;
40
+ text?: string;
41
+ textMuted?: string;
42
+ textFaint?: string;
43
+ border?: string;
44
+ borderStrong?: string;
45
+ accent?: string;
46
+ accentStrong?: string;
47
+ accentSoft?: string;
48
+ onAccent?: string;
49
+ good?: string;
50
+ warn?: string;
51
+ danger?: string;
52
+ chart1?: string;
53
+ chart2?: string;
54
+ chart3?: string;
55
+ chart4?: string;
56
+ chart5?: string;
57
+ chart6?: string;
58
+ chartPositive?: string;
59
+ chartNegative?: string;
60
+ /** Admin/application content width, e.g. `"960px"`. */
61
+ contentWidth?: string;
62
+ panelRadius?: string;
63
+ panelShadow?: string;
64
+ masterDetailColumns?: string;
65
+ masterDetailMinHeight?: string;
66
+ /** Chapter-admin page padding shorthand. */
67
+ pagePadding?: string;
68
+ /** Chapter-admin workspace-tab padding shorthand. */
69
+ workspacePadding?: string;
70
+ }
71
+ /** Brand tokens that theme the site: theme, semantic roles, fonts, and logos. */
36
72
  interface ChapterBrand {
73
+ /** Preloaded @odla-ai/ui theme name used by ThemeScope. */
74
+ theme?: string;
75
+ /** Admin/application color mode. Explicit light is the safe default. */
76
+ colorScheme?: "light" | "dark" | "system";
77
+ tokens?: ChapterBrandTokens;
78
+ tokensDark?: ChapterBrandTokens;
37
79
  /** Palette overrides written into styles.css :root (e.g. `{ moss: "#2F3E34" }`
38
80
  * or `--ui-*` token names). Applies in BOTH light and dark unless overridden
39
81
  * by {@link paletteDark}. */
@@ -67,7 +109,7 @@ interface ChapterNetworkTarget {
67
109
  id: string;
68
110
  /** Human-facing site name. Defaults to `id`. */
69
111
  name?: string;
70
- /** Absolute follower origin, e.g. `https://silver.example.com`. */
112
+ /** Absolute follower origin, e.g. `https://chapter.example.com`. */
71
113
  url: string;
72
114
  /** Leader-vault key holding this follower's share secret. Defaults to
73
115
  * `network_share_<id-with-underscores>`. */
@@ -138,10 +180,10 @@ interface ChapterScheduling {
138
180
  }
139
181
  /** How a signed-in user's role is determined.
140
182
  *
141
- * - `"claim"` reads a role ladder from a JWT claim (Silver & Salt's model:
183
+ * - `"claim"` reads a role ladder from a JWT claim (the member-site model:
142
184
  * `provisional → member → admin`). Admin is the last (highest) rung; the
143
185
  * read-only `superAdmins` tier sits above it.
144
- * - `"table"` gates on the deny-all `admins` allowlist (Built Not Found's model):
186
+ * - `"table"` gates on the deny-all `admins` allowlist (the hub model):
145
187
  * a binary "email is an admin or isn't", set only in odla Studio.
146
188
  *
147
189
  * Defaults: `"claim"` in `chapter` mode, `"table"` in `hub` mode. */
@@ -168,7 +210,7 @@ interface ResolvedAuth {
168
210
  }
169
211
  /** The application status pipeline. Which statuses exist, and the subsets a site
170
212
  * allows a call to be booked from / an application approved from. Defaults to
171
- * Silver & Salt's pipeline. Status never moves backwards (package-enforced). */
213
+ * the reference pipeline. Status never moves backwards (package-enforced). */
172
214
  interface ChapterPipeline {
173
215
  stages?: readonly string[];
174
216
  bookableFrom?: readonly string[];
@@ -185,8 +227,8 @@ interface ResolvedPipeline {
185
227
  }
186
228
  /** The application (join form) validation surface — which string fields are
187
229
  * required vs accepted, their max lengths, and the request body cap. Drives
188
- * submit validation + the CRM slot projection; defaults to Silver & Salt's form.
189
- * The `applications` schema attrs stay fixed (byte-equal to S&S); this is
230
+ * submit validation + the CRM slot projection; defaults to the reference form.
231
+ * The `applications` schema attrs stay fixed; this is
190
232
  * validation config, not schema generation. */
191
233
  interface ChapterApplication {
192
234
  required?: readonly string[];
@@ -258,7 +300,7 @@ interface ChapterConfig {
258
300
  /** `notificationEmail` required in `chapter` mode. */
259
301
  emails?: ChapterEmails;
260
302
  scheduling?: ChapterScheduling;
261
- /** Application status pipeline (stages + bookable/approvable subsets). Defaults to S&S's. */
303
+ /** Application status pipeline (stages + bookable/approvable subsets). */
262
304
  pipeline?: ChapterPipeline;
263
305
  /** Join-form validation (required/optional fields, max lengths, body cap). */
264
306
  application?: ChapterApplication;
@@ -32,8 +32,50 @@ interface Rule {
32
32
  }
33
33
  /** Namespace → rule set. */
34
34
  type DbRules = Record<string, Rule>;
35
- /** Brand tokens that theme the site: palette, fonts, wordmark, nav, logos. */
35
+ /** Typed semantic roles shared by the public site and application surfaces. */
36
+ interface ChapterBrandTokens {
37
+ background?: string;
38
+ surface?: string;
39
+ surface2?: string;
40
+ text?: string;
41
+ textMuted?: string;
42
+ textFaint?: string;
43
+ border?: string;
44
+ borderStrong?: string;
45
+ accent?: string;
46
+ accentStrong?: string;
47
+ accentSoft?: string;
48
+ onAccent?: string;
49
+ good?: string;
50
+ warn?: string;
51
+ danger?: string;
52
+ chart1?: string;
53
+ chart2?: string;
54
+ chart3?: string;
55
+ chart4?: string;
56
+ chart5?: string;
57
+ chart6?: string;
58
+ chartPositive?: string;
59
+ chartNegative?: string;
60
+ /** Admin/application content width, e.g. `"960px"`. */
61
+ contentWidth?: string;
62
+ panelRadius?: string;
63
+ panelShadow?: string;
64
+ masterDetailColumns?: string;
65
+ masterDetailMinHeight?: string;
66
+ /** Chapter-admin page padding shorthand. */
67
+ pagePadding?: string;
68
+ /** Chapter-admin workspace-tab padding shorthand. */
69
+ workspacePadding?: string;
70
+ }
71
+ /** Brand tokens that theme the site: theme, semantic roles, fonts, and logos. */
36
72
  interface ChapterBrand {
73
+ /** Preloaded @odla-ai/ui theme name used by ThemeScope. */
74
+ theme?: string;
75
+ /** Admin/application color mode. Explicit light is the safe default. */
76
+ colorScheme?: "light" | "dark" | "system";
77
+ tokens?: ChapterBrandTokens;
78
+ tokensDark?: ChapterBrandTokens;
37
79
  /** Palette overrides written into styles.css :root (e.g. `{ moss: "#2F3E34" }`
38
80
  * or `--ui-*` token names). Applies in BOTH light and dark unless overridden
39
81
  * by {@link paletteDark}. */
@@ -67,7 +109,7 @@ interface ChapterNetworkTarget {
67
109
  id: string;
68
110
  /** Human-facing site name. Defaults to `id`. */
69
111
  name?: string;
70
- /** Absolute follower origin, e.g. `https://silver.example.com`. */
112
+ /** Absolute follower origin, e.g. `https://chapter.example.com`. */
71
113
  url: string;
72
114
  /** Leader-vault key holding this follower's share secret. Defaults to
73
115
  * `network_share_<id-with-underscores>`. */
@@ -138,10 +180,10 @@ interface ChapterScheduling {
138
180
  }
139
181
  /** How a signed-in user's role is determined.
140
182
  *
141
- * - `"claim"` reads a role ladder from a JWT claim (Silver & Salt's model:
183
+ * - `"claim"` reads a role ladder from a JWT claim (the member-site model:
142
184
  * `provisional → member → admin`). Admin is the last (highest) rung; the
143
185
  * read-only `superAdmins` tier sits above it.
144
- * - `"table"` gates on the deny-all `admins` allowlist (Built Not Found's model):
186
+ * - `"table"` gates on the deny-all `admins` allowlist (the hub model):
145
187
  * a binary "email is an admin or isn't", set only in odla Studio.
146
188
  *
147
189
  * Defaults: `"claim"` in `chapter` mode, `"table"` in `hub` mode. */
@@ -168,7 +210,7 @@ interface ResolvedAuth {
168
210
  }
169
211
  /** The application status pipeline. Which statuses exist, and the subsets a site
170
212
  * allows a call to be booked from / an application approved from. Defaults to
171
- * Silver & Salt's pipeline. Status never moves backwards (package-enforced). */
213
+ * the reference pipeline. Status never moves backwards (package-enforced). */
172
214
  interface ChapterPipeline {
173
215
  stages?: readonly string[];
174
216
  bookableFrom?: readonly string[];
@@ -185,8 +227,8 @@ interface ResolvedPipeline {
185
227
  }
186
228
  /** The application (join form) validation surface — which string fields are
187
229
  * required vs accepted, their max lengths, and the request body cap. Drives
188
- * submit validation + the CRM slot projection; defaults to Silver & Salt's form.
189
- * The `applications` schema attrs stay fixed (byte-equal to S&S); this is
230
+ * submit validation + the CRM slot projection; defaults to the reference form.
231
+ * The `applications` schema attrs stay fixed; this is
190
232
  * validation config, not schema generation. */
191
233
  interface ChapterApplication {
192
234
  required?: readonly string[];
@@ -258,7 +300,7 @@ interface ChapterConfig {
258
300
  /** `notificationEmail` required in `chapter` mode. */
259
301
  emails?: ChapterEmails;
260
302
  scheduling?: ChapterScheduling;
261
- /** Application status pipeline (stages + bookable/approvable subsets). Defaults to S&S's. */
303
+ /** Application status pipeline (stages + bookable/approvable subsets). */
262
304
  pipeline?: ChapterPipeline;
263
305
  /** Join-form validation (required/optional fields, max lengths, body cap). */
264
306
  application?: ChapterApplication;
@@ -1138,7 +1138,7 @@ function createStripeProvider(config) {
1138
1138
  "expand[0]": "latest_invoice.confirmation_secret",
1139
1139
  metadata: meta
1140
1140
  },
1141
- // The hardening S&S lacked: one subscription per application, so a client
1141
+ // Hardening: one subscription per application, so a client
1142
1142
  // retry between create and the db write can't orphan a second one.
1143
1143
  `sub:${input.applicationId}`
1144
1144
  );
@@ -1709,18 +1709,30 @@ var handleAdminPeopleRole = async (req, url, env, ctx) => {
1709
1709
  };
1710
1710
 
1711
1711
  // src/series.ts
1712
- function bucketSeries(points, now, weeks = 12) {
1713
- const WEEK = 7 * 864e5;
1714
- const end = now;
1715
- const start = end - weeks * WEEK;
1716
- const buckets = Array.from({ length: weeks }, (_, i) => ({ weekStart: start + i * WEEK, value: 0 }));
1717
- for (const p of points) {
1718
- if (!Number.isFinite(p.t) || p.t < start || p.t > end) continue;
1719
- const idx = Math.min(weeks - 1, Math.floor((p.t - start) / WEEK));
1720
- const bucket = buckets[idx];
1721
- if (bucket) bucket.value += Number.isFinite(p.v) ? p.v : 0;
1722
- }
1723
- return buckets;
1712
+ function periodSeries(points, now, count, periodMs) {
1713
+ const start = now - count * periodMs;
1714
+ const previousStart = start - count * periodMs;
1715
+ const current = Array.from({ length: count }, () => 0);
1716
+ const previous = Array.from({ length: count }, () => 0);
1717
+ for (const point of points) {
1718
+ if (!Number.isFinite(point.t) || !Number.isFinite(point.v)) continue;
1719
+ if (point.t >= start && point.t <= now) {
1720
+ const index = Math.min(count - 1, Math.floor((point.t - start) / periodMs));
1721
+ current[index] = (current[index] ?? 0) + point.v;
1722
+ } else if (point.t >= previousStart && point.t < start) {
1723
+ const index = Math.min(count - 1, Math.floor((point.t - previousStart) / periodMs));
1724
+ previous[index] = (previous[index] ?? 0) + point.v;
1725
+ }
1726
+ }
1727
+ const labels = current.map((_, index) => new Date(start + index * periodMs).toISOString().slice(5, 10));
1728
+ return { labels, current, previous };
1729
+ }
1730
+ function dashboardMetricData(points, now) {
1731
+ const DAY = 864e5;
1732
+ return {
1733
+ wow: periodSeries(points, now, 7, DAY),
1734
+ mom: periodSeries(points, now, 4, 7 * DAY)
1735
+ };
1724
1736
  }
1725
1737
  function subAnnualCents(sub) {
1726
1738
  const items = sub.items?.data ?? [];
@@ -1803,8 +1815,8 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
1803
1815
  if (subsRes.ok) {
1804
1816
  const subs = rows(subsRes.body.data);
1805
1817
  const subMs = (s) => (s.created ?? 0) * 1e3;
1806
- membersSeries = bucketSeries(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
1807
- revenueSeries = bucketSeries(subs.map((s) => ({ t: subMs(s), v: subAnnualCents(s) })), now);
1818
+ membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
1819
+ revenueSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: subAnnualCents(s) })), now);
1808
1820
  const active = subs.filter((s) => s.status === "active");
1809
1821
  revenue = {
1810
1822
  billingReady: true,
@@ -1816,7 +1828,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
1816
1828
  };
1817
1829
  }
1818
1830
  }
1819
- const applicationsSeries = bucketSeries(apps.map((a) => ({ t: a.createdAt || 0, v: 1 })), now);
1831
+ const applicationsSeries = dashboardMetricData(apps.map((a) => ({ t: a.createdAt || 0, v: 1 })), now);
1820
1832
  return json({ applications, applicationsSeries, pipeline, pipelineDelta, calls, agenda, timezone, revenue, revenueSeries, membersSeries });
1821
1833
  };
1822
1834
  var handleAdminBilling = async (req, url, env, ctx) => {