@wenathlan/cobaia 2.0.165 → 2.0.167

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 (51) hide show
  1. package/.dockerignore +1 -1
  2. package/App.tsx +2 -31
  3. package/Dockerfile +162 -4
  4. package/biome.jsonc +3 -3
  5. package/build.gradle.kts +4 -4
  6. package/bun.lock +1820 -487
  7. package/cobaia.csproj +2 -2
  8. package/cobaia.gemspec +2 -2
  9. package/cobaia.java +2 -2
  10. package/db.ts +87 -3
  11. package/docker-compose.yml +58 -0
  12. package/docker.config +74 -0
  13. package/docs/cobaia.md +31 -0
  14. package/docs/distributionlanes.md +1 -1
  15. package/keywords.txt +1 -0
  16. package/labels.ts +23 -1
  17. package/monetization.ts +248 -0
  18. package/nginx.conf +106 -0
  19. package/onboardingstate.ts +211 -0
  20. package/package.json +430 -83
  21. package/pnpm-workspace.yaml +60 -38
  22. package/pom.xml +4 -4
  23. package/routerbase.ts +1 -0
  24. package/schema.prisma +23 -0
  25. package/sol/Sol.tsx +20 -15
  26. package/sol/apps/apps.tsx +1 -1
  27. package/sol/blog/blog.tsx +1 -1
  28. package/sol/home/home.tsx +14 -4
  29. package/sol/intro/intro.tsx +13 -5
  30. package/sol/onboarding/onboarding.tsx +146 -0
  31. package/sol/pricing/pricing.tsx +1 -1
  32. package/sol/shell/Shell.tsx +1 -1
  33. package/sol/sol.css +153 -0
  34. package/sol/support/support.tsx +1 -1
  35. package/sol/wingets/HomeEntrygate.tsx +64 -0
  36. package/sol/wingets/IntroFooter.tsx +1 -1
  37. package/sol/wingets/IntroPlans.tsx +1 -1
  38. package/sol/wingets/ShellAdsslot.tsx +1 -1
  39. package/sol/wingets/index.ts +3 -1
  40. package/tauri.conf.json +1 -1
  41. package/{tests → test}/adsconfig.test.ts +2 -2
  42. package/{tests → test}/checkoutlinks.test.ts +2 -2
  43. package/test/entryflow.test.ts +124 -0
  44. package/{tests → test}/familyurl.test.ts +1 -1
  45. package/{tests → test}/posts.test.ts +2 -2
  46. package/{tests → test}/sandboxmodality.test.ts +1 -1
  47. package/vitest.config.ts +1 -1
  48. package/adsconfig.ts +0 -59
  49. package/checkoutlinks.ts +0 -80
  50. package/posts.ts +0 -54
  51. package/roster.ts +0 -29
package/cobaia.csproj CHANGED
@@ -4,7 +4,7 @@
4
4
  <AssemblyName>cobaia</AssemblyName>
5
5
  <RootNamespace>cobaia</RootNamespace>
6
6
  <PackageId>cobaia</PackageId>
7
- <Version>2.0.165</Version>
7
+ <Version>2.0.167</Version>
8
8
  <Authors>wenathlan</Authors>
9
9
  <Description>Distribution package containing the cobaia documentation set and the tests directory. cobaia is the monetization surface — the plans, the support rails and the honest advertisement slots.</Description>
10
10
  <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
@@ -17,6 +17,6 @@
17
17
  <ItemGroup>
18
18
  <None Include="README.md" Pack="true" PackagePath="/" />
19
19
  <None Include="docs/**/*" Pack="true" PackagePath="contentFiles/any/any/docs/" />
20
- <None Include="tests/**/*" Pack="true" PackagePath="contentFiles/any/any/tests/" />
20
+ <None Include="test/**/*" Pack="true" PackagePath="contentFiles/any/any/test/" />
21
21
  </ItemGroup>
22
22
  </Project>
package/cobaia.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "cobaia"
3
- spec.version = ENV.fetch("COBAIA_VERSION", "2.0.165")
3
+ spec.version = ENV.fetch("COBAIA_VERSION", "2.0.167")
4
4
  spec.authors = ["wenathlan"]
5
5
  spec.email = ["support@users.noreply.github.com"]
6
6
  spec.summary = "The cobaia documentation and tests envelope (the monetization surface — the plans, the support rails and the honest advertisement slots)."
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.homepage = "https://github.com/wenathlan/devthink"
9
9
  spec.license = "GPL-3.0-only"
10
10
  spec.required_ruby_version = ">= 3.1"
11
- spec.files = Dir["README.md", "docs/**/*", "tests/**/*"]
11
+ spec.files = Dir["README.md", "docs/**/*", "test/**/*"]
12
12
  spec.metadata["source_code_uri"] = "https://github.com/wenathlan/devthink"
13
13
  spec.metadata["homepage_uri"] = spec.homepage
14
14
  end
package/cobaia.java CHANGED
@@ -1,4 +1,4 @@
1
- package io.github.wenathlan.cobaia;
1
+ package com.wenathlan.cobaia;
2
2
 
3
3
  /**
4
4
  * The cobaia version envelope carrier.
@@ -10,7 +10,7 @@ package io.github.wenathlan.cobaia;
10
10
  */
11
11
  final class Cobaia {
12
12
  /** The version of the cobaia envelope (the six-distribution completeness wave). */
13
- public static final String VERSION = "2.0.165";
13
+ public static final String VERSION = "2.0.167";
14
14
  private Cobaia() {
15
15
  }
16
16
  }
package/db.ts CHANGED
@@ -5,14 +5,15 @@
5
5
  * concatenation. The schema mirrors schema.prisma (the documentation of
6
6
  * record) and the boot DDL is the idempotent seed-on-first-run: the tables
7
7
  * exist after the first open, always. The platform gateway serves the same
8
- * tables over HTTPS (GET/POST /api/v1/onboarding); no server route lives
9
- * here.
8
+ * tables over HTTPS (GET/POST /api/v1/onboarding, GET /api/v1/ui/content);
9
+ * no server route lives here.
10
10
  */
11
11
  import { openStorageLane } from "@wenathlan/vault/storage";
12
+ import { CONTENTKINDS, type contentrow, seedAdsLabel, seedAdsEmpty, seedAdsSlots, seedPlans, seedPosts, seedRoster, seedSupportLinks } from "./content";
12
13
  import { LABEL_MANIFEST, LABEL_REVISION } from "./labels";
13
14
 
14
15
  /** the correlated tables the runtime keeps (the family sync contract names). */
15
- export const tables = ["onboarding_states", "labels"] as const;
16
+ export const tables = ["onboarding_states", "labels", "content_rows"] as const;
16
17
 
17
18
  /** the one storage lane of the site store (WAL, foreign keys, patient locks — the app's own file, the shared mechanism of the vault). */
18
19
  const lane = openStorageLane({ defaultFile: "./cobaia.db" });
@@ -41,6 +42,12 @@ CREATE TABLE IF NOT EXISTS labels (
41
42
  updatedAt TEXT NOT NULL DEFAULT (datetime('now')),
42
43
  translated INTEGER NOT NULL DEFAULT 0
43
44
  );
45
+ CREATE TABLE IF NOT EXISTS content_rows (
46
+ kind TEXT NOT NULL,
47
+ ordinal INTEGER NOT NULL,
48
+ payload TEXT NOT NULL,
49
+ PRIMARY KEY (kind, ordinal)
50
+ );
44
51
  `;
45
52
 
46
53
  /** Creates the tables when they do not exist (idempotent, mirrors the prisma schema). */
@@ -217,3 +224,80 @@ try {
217
224
  } catch {
218
225
  /* the manifest answers; the store catches up on the next boot */
219
226
  }
227
+
228
+ /* --------------------------------------------------------------------- */
229
+ /* content rows — the site content the pages render */
230
+ /* --------------------------------------------------------------------- */
231
+
232
+ /** the manifest seed of one content kind: the rows in ordinal order. */
233
+ function seedrowsOf(kind: (typeof CONTENTKINDS)[number]): readonly unknown[] {
234
+ switch (kind) {
235
+ case "posts":
236
+ return seedPosts;
237
+ case "roster":
238
+ return seedRoster;
239
+ case "plans":
240
+ return seedPlans;
241
+ case "supportlinks":
242
+ return [seedSupportLinks];
243
+ case "ads.slots":
244
+ return [seedAdsSlots];
245
+ case "ads.label":
246
+ return [seedAdsLabel];
247
+ case "ads.empty":
248
+ return [seedAdsEmpty];
249
+ }
250
+ }
251
+
252
+ /** Encodes the manifest rows of every kind as content rows (kind, ordinal,
253
+ * json payload) — the encoding the api answers and the client loader parses. */
254
+ function seedContentRows(): contentrow[] {
255
+ const rows: contentrow[] = [];
256
+ for (const kind of CONTENTKINDS) {
257
+ seedrowsOf(kind).forEach((value, ordinal) => {
258
+ rows.push({ kind, ordinal, payload: JSON.stringify(value) });
259
+ });
260
+ }
261
+ return rows;
262
+ }
263
+
264
+ /**
265
+ * Ingests the content manifest into the content_rows table — idempotent:
266
+ * every manifest row upserts over its (kind, ordinal) seat, so the db stays
267
+ * the single living config of the site content the pages render (the
268
+ * content law: the literals live in the manifest, the rows live here).
269
+ *
270
+ * @returns the number of rows seated.
271
+ */
272
+ export function seedContentTable(): number {
273
+ ensureSchema();
274
+ for (const row of seedContentRows()) {
275
+ execute(
276
+ `INSERT INTO content_rows (kind, ordinal, payload) VALUES (?, ?, ?)
277
+ ON CONFLICT(kind, ordinal) DO UPDATE SET payload = excluded.payload`,
278
+ [row.kind, row.ordinal, row.payload],
279
+ );
280
+ }
281
+ return seedContentRows().length;
282
+ }
283
+
284
+ /**
285
+ * Reads every content row of the store, ordered by kind then ordinal — the
286
+ * rows the platform gateway answers over GET /api/v1/ui/content.
287
+ *
288
+ * @returns the content rows of the site store.
289
+ */
290
+ export function readContentRows(): contentrow[] {
291
+ return queryAll<Pick<contentrow, "kind" | "ordinal" | "payload">>(
292
+ "SELECT kind, ordinal, payload FROM content_rows ORDER BY kind ASC, ordinal ASC",
293
+ ).map((row) => ({ kind: row.kind as contentrow["kind"], ordinal: row.ordinal, payload: row.payload }));
294
+ }
295
+
296
+ /* the content law rides the db boot beside the labels: the manifest rows
297
+ * land in the content_rows table from the first open, always (idempotent,
298
+ * best effort — the manifest keeps answering when the store is sealed). */
299
+ try {
300
+ seedContentTable();
301
+ } catch {
302
+ /* the manifest answers; the store catches up on the next boot */
303
+ }
@@ -0,0 +1,58 @@
1
+ # cobaia docker-compose — the two-service deploy of the monetization face: the
2
+ # static theme served by the cobaia-face nginx door beside the /api/* proxy lane
3
+ # that carries every platform call to the cobaia engine. until this file the
4
+ # app rode the registry image alone: no compose seated the pair, and every
5
+ # /api call of the face answered the away rehearsal. this file wires the
6
+ # pair the vault compose law standardizes:
7
+ #
8
+ # cobaia-engine the cobaia site container the publish lane builds from the staged
9
+ # family context (the app nested under app/ with the vault sibling
10
+ # the copy law carries): the built theme behind the unprivileged
11
+ # nginx, EXPOSE 8080, the wget listen probe the image carries
12
+ # cobaia-face the nginx surface: the family conf (nginx.conf) mounted over
13
+ # the default conf — the resolver law, the security header law,
14
+ # the gzip and cache law, the /api/* proxy lane to the engine,
15
+ # the spa rewrite as try_files, host 8083 published on 8080.
16
+ #
17
+ # the engine image is the one the registry lane publishes (ghcr.io/wenathlan/cobaia:<version>
18
+ # — the publish matrix); the publish matrix builds the same image from the staged family context (the app nested under app/ with the vault sibling the copy law carries):
19
+ #
20
+ # rm -rf familybuildctx && mkdir -p familybuildctx/app
21
+ # cp -a cobaia/. familybuildctx/app/
22
+ # cp -a vault/. familybuildctx/vault/ && rm -rf familybuildctx/vault/node_modules
23
+ # docker build -f cobaia/Dockerfile familybuildctx
24
+ #
25
+ # the /api lane carries the platform calls same-origin: the face proxies
26
+ # them to the cobaia container, and the data lanes the browser contract
27
+ # demands ride the configured gateway base the build stamps (the https law
28
+ # the modules carry).
29
+
30
+ services:
31
+ cobaia-engine:
32
+ image: "ghcr.io/wenathlan/cobaia:${COBAIA_VERSION:-2.0.166}"
33
+ # the engine never publishes a port by default: the face proxies it on
34
+ # the private compose network (the vault precedent). uncomment to serve
35
+ # the engine lane directly behind your own door:
36
+ # ports:
37
+ # - "3213:8080"
38
+ # the healthcheck rides the image (wget -qO- http://127.0.0.1:8080/ —
39
+ # the listen probe the site container carries): the face gates on it below.
40
+ init: true
41
+ restart: unless-stopped
42
+
43
+ cobaia-face:
44
+ image: "ghcr.io/wenathlan/cobaia:${COBAIA_VERSION:-2.0.166}"
45
+ volumes:
46
+ # the family conf over the default site conf: the resolver law, the
47
+ # security headers, the gzip and cache law, the /api proxy lane.
48
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
49
+ depends_on:
50
+ # the face starts only when the engine health lane answered — the
51
+ # proxy upstream is live before the first /api/* call arrives.
52
+ cobaia-engine:
53
+ condition: service_healthy
54
+ ports:
55
+ # the public door: host 8083 → the unprivileged nginx on 8080.
56
+ # remap freely ("8084:8080") when another service owns the host.
57
+ - "8083:8080"
58
+ restart: unless-stopped
package/docker.config ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "meta": {
3
+ "name": "docker.config",
4
+ "title": "cobaia - the container configuration of the monetization site (the compose pair, the nginx face, the site container)",
5
+ "version": "2.0.167",
6
+ "date": "2026-11-06",
7
+ "language": "en",
8
+ "sources": [
9
+ "app:Dockerfile [the cobaia site container — solbuild vite stage + the nginx-unprivileged face, EXPOSE 8080, the wget listen healthcheck]",
10
+ "app:docker-compose.yml [the pair: cobaia-engine + cobaia-face, host 8083:8080, the engine direct lane 3213:8080 unpublished by default]",
11
+ "app:nginx.conf [the vault pattern: resolver 127.0.0.11 valid=10s ipv6=off, location ^~ /api/, the _headers security law, the faun block, the root-emit seal]",
12
+ "app:vite.config.ts [the root-emit outDir law — the hashed bundles land at the application root]",
13
+ "app:package.json [the version stamp the compose args and this manifest carry]",
14
+ "vault:nginx.conf [the reference pattern the family standardizes — the resolver law, the proxy lane, the header law]",
15
+ "saddle:docker.config [the meta format this manifest follows — name, title, version, date, language, sources, mergeReport]",
16
+ "app:content.ts [the platform content lane the /api proxy carries — the https base law]"
17
+ ],
18
+ "mergeReport": {
19
+ "rootFiles": [
20
+ "Dockerfile",
21
+ "docker-compose.yml",
22
+ "nginx.conf",
23
+ "docker.config"
24
+ ],
25
+ "composeServices": [
26
+ "cobaia-engine",
27
+ "cobaia-face"
28
+ ],
29
+ "portMap": {
30
+ "face": "8083:8080",
31
+ "engineDirect": "3213:8080 (unpublished by default — the face proxies the private compose network)"
32
+ },
33
+ "healthLane": "the image healthcheck (wget -qO- http://127.0.0.1:8080/) — the listen probe",
34
+ "conflictResolution": "the vault nginx.conf pattern owns the conf law and the saddle docker.config meta format owns this manifest — one owner per law, the family consumes (the engineowners doctrine)",
35
+ "pinPatch": "the family base nginxinc/nginx-unprivileged:1.29-alpine@sha256:0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6 rides the pattern faces; the static faces ride the registry site image the publish lane stamps"
36
+ }
37
+ },
38
+ "engine": {
39
+ "service": "cobaia-engine",
40
+ "surface": "the cobaia site container the publish lane builds (the staged family context the Dockerfile reads); the image healthcheck probes the listen port (wget http://127.0.0.1:8080/)",
41
+ "port": "8080",
42
+ "healthcheck": "wget -qO- http://127.0.0.1:8080/ (rides the image)",
43
+ "stateHome": "stateless — the site container carries no store",
44
+ "entry": "the site container (nginx serving the built theme)"
45
+ },
46
+ "face": {
47
+ "service": "cobaia-face",
48
+ "image": "ghcr.io/wenathlan/cobaia:2.0.167",
49
+ "surface": "the nginx face: the family conf (nginx.conf) mounted over the default site conf of the same cobaia image — resolver 127.0.0.11, the _headers security law, gzip, the hashed-asset cache, the ^~ /api proxy lane, the faun and root-emit seals, the try_files spa rewrite",
50
+ "listen": 8080,
51
+ "seals": [
52
+ "the faun db block (403)",
53
+ "the root-emit source seal (403)",
54
+ "the dotfile seal (403)",
55
+ "the tree seal (node_modules, tests, docs, sol — 403)"
56
+ ],
57
+ "spaRewrite": "try_files $uri $uri/ /index.html"
58
+ },
59
+ "apiLane": {
60
+ "route": "/api/*",
61
+ "upstream": "http://cobaia-engine:8080",
62
+ "resolver": "127.0.0.11 valid=10s ipv6=off (the docker embedded dns — request-time resolution, the vault law)",
63
+ "body": "client_max_body_size 0 — the engine quota gate is the only ceiling",
64
+ "timeouts": "read 300s / send 300s",
65
+ "carries": "the monetization console rides the platform content lanes (GET /api/v1/ui/content) through the configured https base the build stamps; the /api lane carries the same-origin calls to the cobaia container"
66
+ },
67
+ "ports": {
68
+ "faceHost": 8083,
69
+ "faceInternal": 8080,
70
+ "engineInternal": 8080,
71
+ "engineDirectCommented": 3213,
72
+ "collisionLaw": "every family face owns one distinct host door (80 for vault, 8081-8090 for the ten siblings) — no two composes ever claim the same host port"
73
+ }
74
+ }
package/docs/cobaia.md ADDED
@@ -0,0 +1,31 @@
1
+ # cobaia — the dossier
2
+
3
+ The public monetization surface of the family: plans, MRR tracking, honest ad slots and the support rails.
4
+
5
+ ## Identity
6
+
7
+ - Category: monetization surface.
8
+ - Competitor space: GitHub Sponsors, Ko-fi honest monetization.
9
+
10
+ ## Objective
11
+
12
+ Give the family an honest monetization engine: every plan grants resources (never powers), every ad slot stays empty until approval, every rail is public and auditable.
13
+
14
+ ## Features
15
+
16
+ - Plans: the resource grants the devthink system sells (storage quotas, engine seats).
17
+ - MRR tracking and churn seats.
18
+ - Ad slots that stay empty until the approval flows answer (zero tracking before approval).
19
+ - Support rails: sponsorship links, badges, roster.
20
+ - Billing seats with the honest playbook: cashback becomes store credit, never real money.
21
+ - Absolute ethical limits codified: no invisible click mirroring, no dark patterns.
22
+
23
+ ## Interactions
24
+
25
+ - Public surface with banner, plans and support sections.
26
+ - The roster of supporters and the honest badges.
27
+
28
+ ## Doctrine
29
+
30
+ - Honest monetization: every limit is a plan limit, never an engine limit.
31
+ - Ethical absolute: the ad seat refuses to track before the approval answers.
@@ -6,7 +6,7 @@ The eleven distribution lanes of the PR14 law and the honest status each lane an
6
6
  | --- | --- | --- | --- |
7
7
  | npmjs | package.json — @wenathlan/cobaia | live | the family-npmjs lane publishes the envelope every release rung |
8
8
  | GitHub Packages npm | package.json — @wenathlan/cobaia | live | the family-githubnpm lane publishes the same envelope idempotently |
9
- | Maven | pom.xml — io.github.wenathlan:cobaia | live | the family-maven lane deploys the jar |
9
+ | Maven | pom.xml — com.wenathlan:cobaia | live | the family-maven lane deploys the jar |
10
10
  | NuGet | cobaia.csproj | live | the family-nuget lane packs the envelope |
11
11
  | Gradle | build.gradle.kts — com.wenathlan:cobaia | live | the family-gradle lane publishes the module |
12
12
  | Ruby Gems | cobaia.gemspec | live | the family-rubygems lane pushes the gem |
package/keywords.txt ADDED
@@ -0,0 +1 @@
1
+ cobaia, cobaia app, cobaia devthink, cobaia monetization, devthink os, wenathlan, monetization surface, monetization app, mrr plans, subscription plans, honest ads, ad slots, ad inventory, ad placements, checkout links, github sponsors, ko-fi, support links, donation links, open source funding, creator monetization, posts feed, content publishing, public content, roster page, team roster, pricing page, plan comparison, free plan, pro plan, business plan, revenue dashboard, monetization dashboard, static monetization site, vite monetization app, react monetization app, open source monetization, developer tools monetization, monetization strategy, passive income, funding goals, sponsor tiers, transparent revenue, wenathlan cobaia, devthink family, monetization app 2026
package/labels.ts CHANGED
@@ -26,7 +26,7 @@ import { createLabelStore } from "@wenathlan/vault/labelstore";
26
26
 
27
27
  /** the label revision of the manifest: bump on every copy wave so the seed
28
28
  * refreshes the store rows of the changed strings. */
29
- export const LABEL_REVISION = 4;
29
+ export const LABEL_REVISION = 5;
30
30
 
31
31
  /** the english manifest: the page.widget.key path answers the surface text.
32
32
  * every entry is the verbatim string the wingets and page anchors rendered
@@ -257,6 +257,28 @@ export const LABEL_MANIFEST = {
257
257
  "intro.footer.familyAria": "the family row",
258
258
  "intro.footer.family": "the family",
259
259
  "intro.footer.memberTip": "open {slug} — the door beside this site",
260
+ // the onboarding page anchor (the first-run frames of the entry flow —
261
+ // the standardized intro → onboarding → entrada law the family rides;
262
+ // cobaia's frames present the real offer: what the site is, what it
263
+ // carries, and the hand-over to the content pages)
264
+ "onboarding.aria": "cobaia — the first-run steps",
265
+ "onboarding.title": "cobaia",
266
+ "onboarding.skip": "skip — straight to the site",
267
+ "onboarding.back": "back",
268
+ "onboarding.next": "next",
269
+ "onboarding.enterCta": "enter the site",
270
+ "onboarding.steps.is.eyebrow": "01 — what this is",
271
+ "onboarding.steps.is.title": "The family's front door.",
272
+ "onboarding.steps.is.text": "cobaia is the monetization face of the devthink family — the front door kept apart so the ten applications stay clean. the directory, the plans, the support channels and the update blog live on this one static site.",
273
+ "onboarding.steps.does.eyebrow": "02 — what it carries",
274
+ "onboarding.steps.does.title": "The plans, the support rails, the honest ads.",
275
+ "onboarding.steps.does.text": "the real offer, nothing invented: the three plans with their placeholder prices said out loud, the support rails that activate when the owner wires them, and the reserved ad boxes that stay empty until a network approves the site.",
276
+ "onboarding.steps.enter.eyebrow": "03 — the entrada",
277
+ "onboarding.steps.enter.title": "One responsive site, every screen.",
278
+ "onboarding.steps.enter.text": "the same responsive pages answer on the phone, the tv, the extension and the desktop — the presentation adapts, the content never changes. the entrada is the front door itself.",
279
+ "onboarding.facts.plans": "the plans — us$0, 5 and 19 a month, placeholder prices until billing lands, honest until then",
280
+ "onboarding.facts.support": "the support rails — the sponsor channels, configured the day the owner wires them",
281
+ "onboarding.facts.ads": "the ad slots — reserved boxes, visibly labeled, empty until a network approves the site",
260
282
  // the ad slot widget (the reserved box of the content pages)
261
283
  "ads.slot.reserved": "reserved · {format}",
262
284
  } as const;
@@ -0,0 +1,248 @@
1
+ // # monetization.ts — the one keyword file of the monetization domain: the
2
+ // consolidation law of the root (PR3 of the uploaded rules — one keyword
3
+ // file per domain, "unifique num único módulo as lógicas variantes +
4
+ // correlacionadas que hoje encontram-se espalhadas") merged the four
5
+ // monetization doors of the cobaia site into this seat — the honest ad slot
6
+ // registry (the former adsconfig.ts body below), the typed billing accessor
7
+ // (checkoutlinks.ts), the seed blog accessor (posts.ts) and the family
8
+ // directory accessor (roster.ts) — every export preserved verbatim, zero
9
+ // logic changes, zero behavior changes. The consolidation notes: the doors
10
+ // shared no duplicated helper (each rode its own thin accessors over the
11
+ // content lane), so the one import the four sources repeated — the
12
+ // content.ts door of the uistore chain — rides once at the top; and no two
13
+ // sources double-declared a const or a decl under one name, so every export
14
+ // keeps the exact name the importers always read (the wingets index
15
+ // re-exports the roster pair by name so its surface stays identical). The
16
+ // four source files are deleted; every importer reads this one seat.
17
+
18
+ import {
19
+ type blogpost,
20
+ type checkoutplan,
21
+ contenttable,
22
+ contentvalue,
23
+ type familymember,
24
+ seedPlans,
25
+ seedPosts,
26
+ seedRoster,
27
+ seedSupportLinks,
28
+ type supportlinks as supportlinkstable,
29
+ } from "./content.ts";
30
+
31
+ export type { blogpost, checkoutplan, familymember, supportlinkstable as supportlinks };
32
+
33
+ /* ════════════════════════════════════════════════════════════════════ */
34
+ /* SECTION: adsconfig.ts — the honest ad slot registry (the ad seat). */
35
+ /* ════════════════════════════════════════════════════════════════════ */
36
+
37
+ // # adsconfig.ts — the honest ad slot registry of the family site, pure data
38
+ // at the app root. The family site carries ONE reserved ad container per
39
+ // content page (home, the app directory, the blog index), always at the end
40
+ // of the content, always labeled "advertisement", always a fixed aspect
41
+ // ratio so the page never shifts when a real unit arrives.
42
+ //
43
+ // THE SCRIPT RULE: no ad network script lives in this repository. The
44
+ // AdSense (or equivalent) loader is injected AFTER the network approves the
45
+ // site, through the deploy environment only. Until then the slot ids stay
46
+ // empty and the container renders the honest empty note. No network call,
47
+ // no click encouragement, no automated traffic ever touches an ad surface —
48
+ // invalid traffic is the one thing that kills both the ad account and the
49
+ // domain reputation, so the family does not do it.
50
+
51
+ /** one reserved ad slot: the config id and the reserved shape. */
52
+ export type adsslot = {
53
+ /** the ad network slot id — empty until the owner configures it after approval */
54
+ slotid: string;
55
+ /** the display format the reserved box mimics */
56
+ format: string;
57
+ /** the css aspect-ratio value the container reserves (never shifts layout) */
58
+ ratio: string;
59
+ };
60
+
61
+ /** the fixed label every ad container renders, visible and honest. */
62
+ export const ADS_LABEL = "advertisement";
63
+
64
+ /** the honest note the container renders while no slot id is configured. */
65
+ export const ADS_EMPTY = "ad slot not configured yet";
66
+
67
+ /** the reserved slots per content page, keyed by the page folder name. */
68
+ export const ADSSLOTS: Readonly<Record<string, adsslot>> = {
69
+ home: { slotid: "", format: "display", ratio: "728 / 90" },
70
+ apps: { slotid: "", format: "in-feed", ratio: "3 / 1" },
71
+ blog: { slotid: "", format: "in-article", ratio: "3 / 1" },
72
+ };
73
+
74
+ /**
75
+ * Reads the reserved slot of one content page.
76
+ *
77
+ * @param page the page folder name (home, apps, blog).
78
+ * @returns the slot, or undefined when the page carries no slot.
79
+ */
80
+ export function adsslotfor(page: string): adsslot | undefined {
81
+ return ADSSLOTS[page];
82
+ }
83
+
84
+ /**
85
+ * Answers whether a slot is configured with a real network id. An empty
86
+ * (or whitespace) slot id means the container stays an honest placeholder.
87
+ *
88
+ * @param slot the reserved slot entry.
89
+ * @returns true only when the slot id carries a value.
90
+ */
91
+ export function adsslotlive(slot: adsslot | undefined): boolean {
92
+ return slot !== undefined && slot.slotid.trim().length > 0;
93
+ }
94
+
95
+ /* ════════════════════════════════════════════════════════════════════ */
96
+ /* SECTION: checkoutlinks.ts — the typed billing accessor (the plans */
97
+ /* seat). */
98
+ /* ════════════════════════════════════════════════════════════════════ */
99
+
100
+ // # checkoutlinks.ts — the typed accessor of the billing configuration of
101
+ // the family site: the thin door the pricing page, the support page and
102
+ // the intro plans zone import, answering the plan table and the support
103
+ // channels of the content lane (content.ts — the manifest; db.ts — the
104
+ // content_rows table; the platform gateway — GET /api/v1/ui/content). The
105
+ // data never lives here anymore: checkoutlinks() and supportlinks() answer
106
+ // the live rows the platform served, the manifest seed otherwise (the
107
+ // uistore chain). The payment links themselves are configuration, never
108
+ // code: the owner pins the checkout hrefs (Stripe Payment Links or Paddle
109
+ // overlays) into the store once billing goes live. Until then every href
110
+ // ships empty and the interface renders the honest empty state — "checkout
111
+ // not configured yet" — instead of pretending.
112
+ //
113
+ // The words never ride here: every plan sentence (name, billing period,
114
+ // blurb, features) answers from the label store (labels.ts — the manifest
115
+ // the seed carries into the site database), so the pricing cards and the
116
+ // intro plans zone read their prose through the typed accessor and this
117
+ // lane keeps only the billing values — ids, prices and links.
118
+ //
119
+ // No network call ever happens here: the links are plain hrefs the visitor
120
+ // chooses to follow, no key is committed (not even a placeholder that looks
121
+ // real) and no script is loaded. The prices are placeholders (US$0 / 5 / 19
122
+ // per month) until the billing platform decision lands.
123
+
124
+ /**
125
+ * The typed accessor of the pricing table: the live db rows when the
126
+ * platform answered, the manifest seed otherwise — in display order (the
127
+ * prose answers from the label store; only the billing values ride this
128
+ * lane).
129
+ *
130
+ * @returns the plans of the /pricing page.
131
+ */
132
+ export function checkoutlinks(): readonly checkoutplan[] {
133
+ return contenttable("plans", seedPlans);
134
+ }
135
+
136
+ /**
137
+ * The typed accessor of the support channels: the live db row when the
138
+ * platform answered, the manifest seed otherwise — the same rule as the
139
+ * plans: hrefs stay empty until the owner configures them.
140
+ *
141
+ * @returns the support channels of the /support page.
142
+ */
143
+ export function supportlinks(): Readonly<supportlinkstable> {
144
+ return contentvalue("supportlinks", seedSupportLinks);
145
+ }
146
+
147
+ /**
148
+ * Answers whether a configured link is live. An empty (or whitespace) href
149
+ * means the feature is not configured yet and the interface must say so.
150
+ *
151
+ * @param href the configured link href.
152
+ * @returns true only when the href carries a real target.
153
+ */
154
+ export function isconfigured(href: string): boolean {
155
+ return href.trim().length > 0;
156
+ }
157
+
158
+ /**
159
+ * Finds one plan of the table by its stable id.
160
+ *
161
+ * @param id the plan id (free, pro, studio).
162
+ * @returns the plan, or undefined when no plan carries the id.
163
+ */
164
+ export function planbyid(id: string): checkoutplan | undefined {
165
+ return checkoutlinks().find((plan) => plan.id === id);
166
+ }
167
+
168
+ /* ════════════════════════════════════════════════════════════════════ */
169
+ /* SECTION: posts.ts — the seed blog accessor (the blog seat). */
170
+ /* ════════════════════════════════════════════════════════════════════ */
171
+
172
+ // # posts.ts — the typed accessor of the seed blog: the thin door the blog
173
+ // page and the tests import, answering the posts of the content lane
174
+ // (content.ts — the manifest; db.ts — the content_rows table; the platform
175
+ // gateway — GET /api/v1/ui/content). The data never lives here anymore:
176
+ // posts() answers the live rows the platform served, the manifest seed
177
+ // otherwise (the uistore chain), the index sort and the honest word count
178
+ // stay exactly where they rode, and the blog index (/blog) renders the
179
+ // sorted index and the full bodies through this door — real, written
180
+ // content about how the family is built, so the site is useful on its own
181
+ // merits (the ad networks approve content, not containers). New posts
182
+ // append to the manifest; the index sorts by date, so the manifest order
183
+ // never matters.
184
+
185
+ /**
186
+ * The typed accessor of the blog: the live db rows when the platform
187
+ * answered, the manifest seed otherwise.
188
+ *
189
+ * @returns the posts of the blog.
190
+ */
191
+ export function posts(): readonly blogpost[] {
192
+ return contenttable("posts", seedPosts);
193
+ }
194
+
195
+ /**
196
+ * Builds the blog index: the posts sorted by date, newest first. The ISO
197
+ * date sort is lexicographic and stable, so the index order never depends
198
+ * on the row order the store answers.
199
+ *
200
+ * @returns the posts of the blog, newest first.
201
+ */
202
+ export function postindex(): readonly blogpost[] {
203
+ return [...posts()].sort((a, b) => {
204
+ if (a.date < b.date) return 1;
205
+ if (a.date > b.date) return -1;
206
+ return 0;
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Counts the words of one post's body — the honest length measure the tests
212
+ * and the page footer can both read.
213
+ *
214
+ * @param post the post to measure.
215
+ * @returns the total word count across the body paragraphs.
216
+ */
217
+ export function postwords(post: blogpost): number {
218
+ return post.body.join(" ").split(/\s+/).filter(Boolean).length;
219
+ }
220
+
221
+ /* ════════════════════════════════════════════════════════════════════ */
222
+ /* SECTION: roster.ts — the family directory accessor (the directory */
223
+ /* seat). */
224
+ /* ════════════════════════════════════════════════════════════════════ */
225
+
226
+ // # roster.ts — the typed accessor of the family directory data: the thin
227
+ // door the /apps page, the shell footer, the home preview and the intro
228
+ // footer import, answering the roster of the content lane (content.ts —
229
+ // the manifest; db.ts — the content_rows table; the platform gateway — GET
230
+ // /api/v1/ui/content). The data never lives here anymore: roster() answers
231
+ // the live rows the platform served, the manifest seed otherwise (the
232
+ // uistore chain). Every deploy unit beside the cobaia, with the one accent
233
+ // each carries (the spec §12 table, mirrored from the argan contract; the
234
+ // devthink OS rides the family sun) and the honest role each app declares
235
+ // in its own devthink.toml — condensed to one line, nothing embellished:
236
+ // the page and the tests read the same table through this door, so the
237
+ // directory can never drift from the family.
238
+
239
+ /**
240
+ * The typed accessor of the directory: the live db rows when the platform
241
+ * answered, the manifest seed otherwise — rail order, the OS first, then
242
+ * the applications.
243
+ *
244
+ * @returns the directory roster.
245
+ */
246
+ export function roster(): readonly familymember[] {
247
+ return contenttable("roster", seedRoster);
248
+ }