@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.
- package/.dockerignore +1 -1
- package/App.tsx +2 -31
- package/Dockerfile +162 -4
- package/biome.jsonc +3 -3
- package/build.gradle.kts +4 -4
- package/bun.lock +1820 -487
- package/cobaia.csproj +2 -2
- package/cobaia.gemspec +2 -2
- package/cobaia.java +2 -2
- package/db.ts +87 -3
- package/docker-compose.yml +58 -0
- package/docker.config +74 -0
- package/docs/cobaia.md +31 -0
- package/docs/distributionlanes.md +1 -1
- package/keywords.txt +1 -0
- package/labels.ts +23 -1
- package/monetization.ts +248 -0
- package/nginx.conf +106 -0
- package/onboardingstate.ts +211 -0
- package/package.json +430 -83
- package/pnpm-workspace.yaml +60 -38
- package/pom.xml +4 -4
- package/routerbase.ts +1 -0
- package/schema.prisma +23 -0
- package/sol/Sol.tsx +20 -15
- package/sol/apps/apps.tsx +1 -1
- package/sol/blog/blog.tsx +1 -1
- package/sol/home/home.tsx +14 -4
- package/sol/intro/intro.tsx +13 -5
- package/sol/onboarding/onboarding.tsx +146 -0
- package/sol/pricing/pricing.tsx +1 -1
- package/sol/shell/Shell.tsx +1 -1
- package/sol/sol.css +153 -0
- package/sol/support/support.tsx +1 -1
- package/sol/wingets/HomeEntrygate.tsx +64 -0
- package/sol/wingets/IntroFooter.tsx +1 -1
- package/sol/wingets/IntroPlans.tsx +1 -1
- package/sol/wingets/ShellAdsslot.tsx +1 -1
- package/sol/wingets/index.ts +3 -1
- package/tauri.conf.json +1 -1
- package/{tests → test}/adsconfig.test.ts +2 -2
- package/{tests → test}/checkoutlinks.test.ts +2 -2
- package/test/entryflow.test.ts +124 -0
- package/{tests → test}/familyurl.test.ts +1 -1
- package/{tests → test}/posts.test.ts +2 -2
- package/{tests → test}/sandboxmodality.test.ts +1 -1
- package/vitest.config.ts +1 -1
- package/adsconfig.ts +0 -59
- package/checkoutlinks.ts +0 -80
- package/posts.ts +0 -54
- 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.
|
|
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="
|
|
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.
|
|
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/**/*", "
|
|
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
|
|
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.
|
|
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);
|
|
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 —
|
|
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 =
|
|
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;
|
package/monetization.ts
ADDED
|
@@ -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
|
+
}
|