Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/(site)/(directories)/ad-networks/card-counts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PageCardCount } from "@/app/ui/cards/page-card";
import { getAdNetworkDirectoryData } from "@/lib/ad-networks";

export async function cardCounts(): Promise<PageCardCount[]> {
const { networks_tracked, listed } = await getAdNetworkDirectoryData();
return [
{ label: "networks", value: networks_tracked },
{ label: "sites", value: listed },
];
}
2 changes: 2 additions & 0 deletions app/(site)/(directories)/card-counts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PageCardCount } from "@/app/ui/cards/page-card";
import { cardCounts as adNetworks } from "./ad-networks/card-counts";
import { cardCounts as cliApps } from "./cli-apps/card-counts";
import { cardCounts as eppStatusCodes } from "./epp-status-codes/card-counts";
import { cardCounts as keyboardShortcuts } from "./keyboard-shortcuts/card-counts";
Expand All @@ -10,6 +11,7 @@ import { cardCounts as tldDirectory } from "./tld-directory/card-counts";

/** Live homepage card counts. Each directory computes its own from its records. */
const counts: Record<string, () => Promise<PageCardCount[]>> = {
"ad-networks": adNetworks,
"cli-apps": cliApps,
"epp-status-codes": eppStatusCodes,
"keyboard-shortcuts": keyboardShortcuts,
Expand Down
11 changes: 11 additions & 0 deletions app/(site)/art/site--spot-ad-networks.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "site--spot-ad-networks",
"type": "toolSpot",
"takeaway": "Website after website carries an ad slot, and an ad network fills each one.",
"source": "conceptual figure, no external data",
"dataAsOf": "2026-10-21",
"width": 140,
"height": 110,
"seed": 127,
"data": { "motif": "adSlotSites" }
}
11 changes: 11 additions & 0 deletions app/(site)/art/site--spot-ad-networks.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions app/(site)/art/site--spot-ad-networks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// generated by scripts/stack127/marker-art/generate.mjs. do not edit.
// edit the sibling .spec.json and rerun: npm run stack127:marker-art
export const siteSpotAdNetworksArt = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 110" role="img" aria-labelledby="ma-title-site--spot-ad-networks" data-marker-art="site--spot-ad-networks">
<title id="ma-title-site--spot-ad-networks">Website after website carries an ad slot, and an ad network fills each one.</title>
<path d="M35.05 8.66 C69.51 10.27, 106.49 9.79, 132.55 9.20 M34.66 10.23 C65.27 10.25, 95.01 9.46, 131.91 10.29 M131.18 10.16 C134.05 23.48, 132.25 38.48, 132.76 73.24 M131.49 9.52 C131.82 31.24, 131.44 52.73, 131.81 73.44" stroke-width="1.4" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
<path d="M24.14 19.03 C60.85 16.66, 97.64 17.06, 123.51 19.11 M23.34 17.64 C56.91 18.55, 90.06 18.90, 122.24 18.10 M120.81 18.36 C122.57 32.44, 121.32 46.72, 122.28 84.15 M122.53 17.68 C121.90 44.53, 122.33 70.29, 121.34 84.53" stroke-width="1.6" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
<path d="M11.13 26.23 C47.57 24.44, 84.94 25.89, 111.19 25.81 M11.57 26.74 C47.24 26.97, 82.94 26.85, 112.65 25.89 M110.24 26.64 C112.14 42.79, 111.36 61.11, 111.76 101.18 M111.97 25.82 C112.58 46.13, 112.19 66.08, 112.52 100.01 M112.87 100.41 C86.39 101.50, 60.42 100.36, 11.55 99.38 M112.00 99.64 C89.26 100.39, 67.38 101.47, 11.49 100.32 M13.00 100.65 C12.72 81.10, 12.51 63.96, 12.38 26.11 M11.26 99.39 C11.67 84.51, 12.06 68.46, 11.44 26.71" stroke-width="1.9" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-ink,#f2f2ee);fill:none"/>
<path d="M13.13 38.53 C49.09 40.34, 86.22 39.57, 112.18 37.91 M11.93 39.05 C49.11 38.67, 86.64 38.25, 111.49 38.78" stroke-width="1.4" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-ink,#f2f2ee);fill:none"/>
<path d="M19.87 55.34 C21.00 55.08, 26.44 53.32, 31.64 47.08 M19.50 55.80 C24.17 53.56, 27.96 49.68, 30.48 47.99 M23.87 60.74 C27.76 58.05, 29.49 51.91, 31.35 49.19 M24.93 61.22 C26.15 58.24, 27.60 54.92, 31.07 48.82 M19.32 64.37 C23.85 60.85, 31.59 56.65, 38.58 47.16 M19.62 66.52 C26.53 60.20, 33.27 54.67, 41.19 48.48 M27.16 72.09 C31.98 61.81, 36.62 56.92, 39.56 48.56 M24.84 70.04 C29.52 67.19, 31.89 61.49, 40.74 48.48 M29.56 67.95 C38.34 59.39, 43.71 54.00, 48.53 50.04 M29.00 66.74 C37.10 60.67, 43.87 53.97, 50.14 47.49 M36.32 71.18 C38.00 65.56, 44.21 56.07, 47.66 47.57 M34.46 71.39 C38.98 66.09, 42.08 60.38, 49.89 48.57 M39.23 66.02 C46.29 57.27, 54.61 50.39, 57.44 47.68 M37.89 66.38 C43.74 60.91, 47.77 57.42, 59.49 47.13 M42.77 69.70 C47.39 64.26, 50.33 58.80, 58.30 46.95 M43.57 70.69 C47.50 65.28, 52.74 58.20, 58.63 48.41 M47.31 64.58 C55.72 59.86, 60.93 54.97, 68.18 47.15 M48.18 65.87 C53.21 60.76, 60.34 55.02, 69.36 47.42 M51.80 70.12 C54.06 67.21, 61.15 61.63, 68.13 46.73 M52.04 71.00 C57.00 64.51, 61.26 57.20, 69.31 48.42 M56.91 67.02 C59.95 63.88, 65.83 58.89, 76.66 47.68 M55.95 66.67 C63.74 60.57, 70.17 54.15, 78.55 48.66 M63.80 69.26 C69.12 63.38, 73.93 54.54, 77.89 48.07 M62.44 71.93 C66.18 65.35, 69.12 60.41, 77.44 46.80 M65.88 67.39 C73.20 60.39, 79.56 54.05, 87.26 49.64 M67.18 65.54 C72.38 61.06, 75.98 57.50, 86.88 47.45 M72.93 71.63 C75.41 65.21, 79.64 62.34, 87.72 46.31 M71.70 69.91 C77.67 62.61, 82.85 55.11, 86.09 47.56 M74.19 67.41 C80.72 61.47, 87.84 55.50, 95.42 47.81 M76.05 66.57 C81.14 60.97, 87.86 56.18, 96.90 46.95 M82.45 70.40 C85.11 64.21, 91.12 54.97, 95.89 48.19 M81.49 70.15 C85.24 64.15, 89.28 57.77, 95.53 48.75 M85.87 65.92 C88.59 60.41, 94.44 59.50, 102.43 53.18 M85.92 65.60 C89.42 62.50, 93.21 57.99, 101.58 51.63 M90.69 68.89 C96.62 62.90, 98.03 55.21, 101.45 53.63 M91.15 71.15 C94.80 64.22, 100.02 55.42, 102.94 51.22 M93.50 66.93 C97.85 64.62, 99.57 63.77, 101.83 62.36 M94.07 66.20 C97.66 64.86, 100.82 62.98, 102.68 62.11 M99.11 70.22 C99.57 68.10, 100.96 65.85, 102.24 61.87 M99.83 70.72 C100.46 68.14, 101.06 65.44, 101.87 62.72" stroke-width="1.3" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-primary,#bafc3c);fill:none"/>
<path d="M22.74 46.18 C53.34 48.26, 82.85 48.09, 103.51 48.57 M22.23 47.35 C54.43 47.85, 85.78 48.58, 101.56 47.38 M103.12 49.21 C100.20 54.54, 100.72 58.59, 100.52 67.58 M102.56 48.20 C101.38 52.83, 103.00 56.95, 101.39 68.71 M101.07 68.03 C80.92 69.08, 55.96 66.73, 21.17 66.50 M102.11 67.32 C71.43 69.01, 41.77 69.68, 21.31 68.44 M21.67 68.16 C21.69 63.64, 21.99 59.71, 23.60 49.71 M21.62 67.32 C22.73 61.83, 22.17 53.86, 22.26 48.76" stroke-width="2" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-primary,#bafc3c);fill:none"/>
<path d="M21.76 79.69 C47.58 79.01, 73.29 79.58, 91.23 78.38 M22.35 79.37 C37.35 79.04, 52.18 79.14, 91.99 78.48" stroke-width="1.5" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
<path d="M21.08 89.07 C40.42 88.11, 57.84 89.34, 69.95 89.81 M22.56 88.93 C33.24 88.45, 45.27 88.20, 70.24 89.53" stroke-width="1.5" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
</svg>`;
20 changes: 16 additions & 4 deletions content_docs/ad-networks/production-runbook.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ad-network production runbook

Ad-network collectors, syncs, snapshots, and backups run as cron jobs on OVH. Weekly niche sorting runs on homelab. All ad-network pages stay unpublished and return 404. Resuming automation does not authorize publication.
Ad-network collectors, syncs, snapshots, and backups run as cron jobs on OVH. Weekly niche sorting runs on homelab. The directory, stats, 21 niche pages, 44 network pages, and checker were published on October 21, 2026. Snigel stays retired and returns 404. Automation updates existing pages; it does not publish new ones.

Cron is running again since October 20, 2026, 15:00 UTC, on self-hosted Postgres 17. It was paused from October 19, 12:15 UTC, until the sync sent only changed rows. History: [OVH cron plan](../../dev-space/ad-networks-ovh-cron-plan.md).

Expand All @@ -20,6 +20,18 @@ The env file also sets `GITHUB_ALERT_TOKEN`, `AD_NETWORK_DB_PATH`, and `AD_NETWO

The wrapper is [`run-job.sh`](../../scripts/ad-networks/ovh/run-job.sh). The full schedule is [`crontab.txt`](../../scripts/ad-networks/ovh/crontab.txt). Homelab's `stack127-ad-network-niche.timer` runs Sunday at 06:00 UTC.

## Counts in titles and headings

The approved titles and headings use rounded site counts. The existing revalidation step refreshes only those count tokens from the target database, including decreases. The cache worker repeats this before processing ad-network events, so a stopped sync or direct data import can recover through the durable queue. No separate schedule is needed.

Editorial wording stays in the page tables. The count refresher recognizes only the approved page and phrase combinations. Descriptions use no site-count snapshots. Publir, EthicalAds, Concert, Sovrn, and Media.net keep their approved count-free titles and headings. The 20-niche taxonomy stays fixed. A refresh within the same rounding bucket makes no page-field update.

Before the new copy is published, older wording is skipped. If a page matches only part of the approved count phrases, the refresh fails and rolls back rather than rewriting its words. Counts below ten lose their numeric prefix and can regain it when the count recovers.

Before preparing a publication plan, render its desired count tokens from that target's current data. Keep the publisher's exact conflict checks: if a scheduled refresh changes a frozen row, prepare a new plan. Check the live page after publishing. A static rebuild still takes several minutes, so data and heading changes reach the public site through the normal verified refresh process.

Restoring an older plan also refuses to overwrite newer count changes. To recover old wording after that conflict, prepare a new plan with the previous wording and current counts. Do not force an old numeric snapshot back into the page tables.

## How a job runs

Collectors use HTTP and SQLite without holding a production connection. Sync then opens one connection, takes the existing runtime and reviewed-sync locks, and pins a read-only SQLite view. It sends only rows whose compact contents differ from the last successful sync. Before each target batch, its old hashes are durably invalidated. New hashes are saved after that batch commits. An interrupted sync cannot reuse an obsolete hash, even if the source later returns to its old value.
Expand Down Expand Up @@ -90,7 +102,7 @@ The coordinator performs these steps after review and release authorization. Kee
ssh ovh '/srv/stack127/scripts/ad-networks/ovh/run-job.sh backup'
```

Check the job logs and Node cache-worker service. The queue must drain without retries or permission errors. Check `/ad-networks`, `/ad-network-checker`, and an existing network child URL: all must return 404.
Check the job logs and Node cache-worker service. The queue must drain without retries or permission errors. Check `/ad-networks`, `/ad-network-checker`, and an existing network child URL: all must return 200. `/ad-networks/snigel` must return 404.
6. Install the new schedule and re-enable the niche timer:

```bash
Expand All @@ -99,7 +111,7 @@ The coordinator performs these steps after review and release authorization. Kee
```

Confirm verification starts at minute 40. Do not install the saved five-minute schedule.
7. Check connections, table sizes, queue age, logs, and disk space after the first hour and first day. Collectors should hold no production session; each sync uses one. Compare the live revision and unpublished HTTP responses again.
7. Check connections, table sizes, queue age, logs, and disk space after the first hour and first day. Collectors should hold no production session; each sync uses one. Compare the live revision and publication responses again.

A local synthetic benchmark scanned 107,146 publishers and 114,284 relationships in 12.4 seconds with no changed rows, zero row payloads, and 12 SQL calls. PostgreSQL responses were simulated; this measures the SQLite scan and hash work. Measure actual OVH sync time during step 4. The old no-change sync took about 12 minutes.

Expand All @@ -125,4 +137,4 @@ ssh homelab 'cd /srv/data/projects/stack127 && node --experimental-strip-types s
ssh ovh '/srv/stack127/scripts/ad-networks/ovh/run-job.sh rdap-import --source-sqlite /srv/stack127-data/state/rdap.sqlite && rm /srv/stack127-data/state/rdap.sqlite'
```

The importer copies only newer RDAP checkpoints and dates, then syncs and refreshes. After its successful backup, disable homelab's old `stack127-ad-network-backup.timer`. This does not affect the niche timer. Pages stay unpublished.
The importer copies only newer RDAP checkpoints and dates, then syncs and refreshes. After its successful backup, disable homelab's old `stack127-ad-network-backup.timer`. This does not affect the niche timer or change which pages are published.
2 changes: 2 additions & 0 deletions lib/card-art.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { siteSpotAdNetworksArt } from "@/app/(site)/art/site--spot-ad-networks";
import { siteSpotPlatformFeesArt } from "@/app/(site)/art/site--spot-platform-fees";
import { siteSpotLaunchDirectoriesArt } from "@/app/(site)/art/site--spot-launch-directories";
import { siteSpotLicensesArt } from "@/app/(site)/art/site--spot-licenses";
Expand Down Expand Up @@ -95,6 +96,7 @@ import { siteSpotStorageToolsArt } from "@/app/(site)/art/site--spot-storage-too

/** Approved generated SVGs keyed by references stored in the content tables. */
const art: Record<string, string> = {
"ad-networks": siteSpotAdNetworksArt,
"cli-apps": siteSpotCliAppsArt,
"epp-status-codes": eppStatusArt,
"keyboard-shortcuts": siteSpotKeyboardShortcutsArt,
Expand Down
1 change: 1 addition & 0 deletions lib/card-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "lucide-react";

const icons: Record<string, LucideIcon> = {
"ad-networks": Network,
"cli-apps": SquareTerminal,
"epp-status-codes": ListChecks,
"keyboard-shortcuts": Keyboard,
Expand Down
1 change: 1 addition & 0 deletions lib/page-registry-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function resolvePagePublicationState<Page extends RegistryPublicati
/** Curated collection, course, reference, and utility pages shown on the homepage. */
export const homePageSlugs = [
"about",
"ad-networks",
"cli-apps",
"epp-status-codes",
"keyboard-shortcuts",
Expand Down
Loading
Loading