Compare commits

...

70 Commits

Author SHA1 Message Date
agent-company b4bd508170 [Phase 0] docs: update STATUS.md with 2026-05-30 sprint plan and critical path
Validate Flux manifests / kustomize-build (pull_request) Failing after 25s
Closes leeworks-agents/api-company#108

- Current date header: 2026-05-30 (sprint planning cycle)
- Critical path summary with ordered operator steps (#47, #90, #76, #77, #4, other secrets)
- New issues #100-#108 summary with completion status
- Backlog size: 28 open issues, all blocked on Phase 0 operator actions
- Docs-site build verification result: npm run build passes, all 9 routes confirmed
- 3 PRs merged this cycle: #103 (pricing), #104 (research log), #105 (publish-openapi CI)
2026-05-30 00:04:44 +00:00
AI-Manager ad679ddc97 Merge pull request '[Phase 1-3] research: seed RESEARCH_LOG.md with first research session (#102)' (#104) from feature/issue-102-research-log into main
Validate Flux manifests / kustomize-build (push) Failing after 8s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 47s
2026-05-30 00:02:31 +00:00
AI-Manager 8518878358 Merge pull request '[Phase 5] docs-site: add /pricing cross-API comparison page (#101)' (#103) from feature/issue-101-pricing into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 35s
Validate Flux manifests / kustomize-build (push) Failing after 16s
2026-05-30 00:02:23 +00:00
AI-Manager be9f80419b Merge pull request '[Phase 6] ci: add scripts/publish-openapi.js and publish-openapi workflow (#100)' (#105) from feature/issue-100-publish-openapi into main
Validate Flux manifests / kustomize-build (push) Failing after 12s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 47s
2026-05-30 00:01:56 +00:00
agent-company 99dae0e710 [Phase 1-3] research: seed RESEARCH_LOG.md with first market research session
Validate Flux manifests / kustomize-build (pull_request) Failing after 11s
Closes leeworks-agents/api-company#102

Documents why ZIP Enrichment, Holidays, and Air Quality were chosen:
- ZIP: 10-15k RapidAPI subscriber demand; USPS/Census free dataset
- Holidays: 50k subs on top competitor (AbstractAPI); Nager.Date gap
- AQI: 8k subs on top competitor; OpenAQ open dataset fills -49/mo gap

RapidAPI competitor subscriber counts documented for all 3 categories.

Identifies 3 new API candidates with data sources and demand evidence:
1. Business Hours API — OSM opening_hours — high revenue potential
2. Vehicle VIN Decoder — NHTSA vPIC (gov data) — high revenue potential
3. Time Zone by Coordinates — timezone-boundary-builder — medium-high

Decision: build VIN Decoder next (strongest paid demand evidence,
government data source, simple API surface).
2026-05-29 20:05:14 +00:00
agent-company 9370d2c898 [Phase 6] ci: add scripts/publish-openapi.js and publish-openapi workflow
Validate Flux manifests / kustomize-build (pull_request) Failing after 8s
Closes leeworks-agents/api-company#100

- scripts/publish-openapi.js: Node.js script using only built-in modules
  (https, fs, path — no extra deps) that uploads each API's openapi.yaml
  to the RapidAPI Platform API via PUT multipart/form-data.
  - Reads RAPIDAPI_KEY/RAPIDAPI_PLATFORM_KEY from environment.
  - Skips silently per API if API ID / version ID not set.
  - Skips all uploads if RAPIDAPI_KEY not set (exits 0).
  - Exits non-zero on any HTTP error.
  - Logs: '✓ Published {api-name} spec to RapidAPI' on success.
  - Passes 'node --check' syntax validation.
- .gitea/workflows/publish-openapi.yaml: triggers on push to main when
  apis/*/openapi.yaml changes.
  - Runs Redocly CLI lint (fails pipeline on spec errors).
  - Detects changed specs via git diff.
  - Calls publish-openapi.js; no secrets = skip, not crash.
2026-05-29 20:04:26 +00:00
agent-company 411ee92106 [Phase 5] docs-site: add /pricing cross-API comparison page and nav link
Validate Flux manifests / kustomize-build (pull_request) Failing after 23s
Closes leeworks-agents/api-company#101

- Create docs-site/src/pages/pricing.astro with accessible table markup
  showing all 4 tiers (Free/Basic/Pro/Ultra) for all 3 APIs
- Add <caption> and scope attributes for accessibility
- Responsive overflow-x wrapper for mobile
- CTA buttons linking to RapidAPI (placeholder # until listings live)
- Revenue disclaimer: subscriptions managed via RapidAPI marketplace
- Add /pricing link to nav in Base.astro layout
- npm run build passes with no errors
2026-05-29 20:03:29 +00:00
AI-Manager 7cfcd0f46b Merge pull request '[Phase 0] ci: add no-op smoke-test workflow to verify Act Runner (#96)' (#98) from feature/issue-96-smoke-test-workflow into main
Validate Flux manifests / kustomize-build (push) Failing after 17s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 29s
2026-05-29 15:03:23 +00:00
agent-company 2f249a9e18 [Phase 0] ci: add no-op smoke-test workflow to verify Act Runner
Validate Flux manifests / kustomize-build (pull_request) Failing after 21s
Adds .gitea/workflows/smoke-test.yaml as a manual workflow_dispatch trigger
that runs four echo commands.  When the Act Runner comes online (#77) the
operator can fire this from the Gitea Actions UI to confirm runner
registration and job dispatch work before any real tooling is needed.

Closes leeworks-agents/api-company#96
2026-05-29 15:03:14 +00:00
AI-Manager cea4658b91 Merge pull request '[Phase 0] flux: add postBuild.substituteFrom for grafana-admin secret (#97)' (#99) from feature/issue-97-flux-postbuild-substitutefrom into main
Validate Flux manifests / kustomize-build (push) Failing after 17s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 44s
2026-05-29 15:03:00 +00:00
agent-company c5a8f9f58c [Phase 0] flux: add postBuild.substituteFrom for grafana-admin secret
Validate Flux manifests / kustomize-build (pull_request) Failing after 15s
Adds postBuild.substituteFrom to the Flux Kustomization reference manifest
so that ${GRAFANA_ADMIN_PASSWORD} in flux/monitoring/helmrelease.yaml is
substituted from the grafana-admin secret at reconcile time.

Without this block Flux passes the literal string to the Helm chart, causing
Grafana to start with a broken admin password.

The live copy in 0xWheatyz/Talos at
testing1/first-cluster/cluster/flux/api-company-source/kustomization.yaml
must also be updated with the same block (companion to issue #90).

Closes leeworks-agents/api-company#97
2026-05-29 15:02:13 +00:00
AI-Manager 8e64a9e2ac Merge pull request '[Phase 0] docs: add docs/tooling.md with tooling versions and git remote config' (#95) from feature/docs-tooling-md into main
Validate Flux manifests / kustomize-build (push) Failing after 21s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 44s
2026-05-29 10:02:32 +00:00
agent-company 348baf96bb docs: add docs/tooling.md with Phase 0 tooling versions and git remote config
Validate Flux manifests / kustomize-build (pull_request) Failing after 22s
Documents Node.js v22.22.2, Python 3.11.2, Helm v3.21.0, Flux CLI v2.4.0,
kustomize v5.6.0 as available in the agent container. Confirms git remote
points to gitea.leeworks.dev/leeworks-agents/api-company. Includes stack
summary (runtime choices, Helm registry, Flux bootstrap reference) and
a live-verification block for operator use.

Closes leeworks-agents/api-company#94
2026-05-29 10:02:06 +00:00
AI-Manager cb9909172b Merge pull request '[Phase 3] fix(flux): move bedag HelmRepository to top-level to fix Flux race condition' (#93) from feature/fix-bedag-helmrepository-87 into main
Validate Flux manifests / kustomize-build (push) Failing after 8s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 43s
2026-05-29 05:06:11 +00:00
AI-Manager 38cddd6dfc Merge pull request '[Phase 4] feat(monitoring): add APIDataCriticallyStale alert and fix APIDataStale for duration' (#92) from feature/add-data-freshness-alerts-88 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 38s
Validate Flux manifests / kustomize-build (push) Failing after 18s
2026-05-29 05:06:00 +00:00
AI-Manager cd0ed86571 Merge pull request '[Phase 5] feat(docs-site): add legal pages (ToS, Privacy Policy, AUP)' (#91) from feature/add-legal-pages-89 into main
Validate Flux manifests / kustomize-build (push) Failing after 5s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 43s
2026-05-29 05:05:55 +00:00
agent-company cb4c0649f5 feat(docs-site): add legal pages (ToS, Privacy Policy, AUP) and footer links
Validate Flux manifests / kustomize-build (pull_request) Failing after 20s
Add three Astro markdown pages under docs-site/src/pages/legal/:
- terms-of-service.md  -> /legal/terms-of-service
- privacy-policy.md    -> /legal/privacy-policy
- acceptable-use-policy.md -> /legal/acceptable-use-policy

All three use the Base.astro layout and render the legal content from
docs/legal/ (synced verbatim). The Base.astro layout already had legal
footer links added previously; this commit also adds a footer section
directly in index.astro for redundancy.

The ToS URL (https://docs.leeworks.dev/legal/terms-of-service) is now
ready to be pasted into the RapidAPI listing (issue #44).

Closes leeworks-agents/api-company#89
2026-05-29 05:05:13 +00:00
agent-company 1a1a109aa3 feat(monitoring): add APIDataCriticallyStale alert and fix APIDataStale for duration
Validate Flux manifests / kustomize-build (pull_request) Failing after 17s
- Fix APIDataStale 'for' duration from 5m to 30m (issue spec requires 30m)
- Add APIDataCriticallyStale alert at 60-day threshold (5184000s) with for: 1h
  and severity: critical

Both alerts fire on api_data_freshness_seconds metric for all three API jobs.
Existing APIHighErrorRate and APIHighLatency alerts are unmodified.
kustomize build flux/ passes.

Closes leeworks-agents/api-company#88
2026-05-29 05:04:56 +00:00
agent-company b616e11bdf fix(flux): move bedag HelmRepository to top-level to fix Flux race condition
Validate Flux manifests / kustomize-build (pull_request) Failing after 13s
The bedag HelmRepository was only declared inside flux/docs-site/, causing
a race condition where zip-enrichment, holidays, and air-quality HelmReleases
would fail with 'HelmRepository not found' if they reconciled before docs-site.

Move flux/docs-site/helmrepository.yaml -> flux/bedag-helmrepository.yaml and
add it as a top-level resource in flux/kustomization.yaml so it is always
available before any HelmRelease that depends on the bedag chart.

Closes leeworks-agents/api-company#87
2026-05-29 05:04:48 +00:00
AI-Manager 618d1acd68 Merge pull request '[Phase 5] Add Flux ImageRepository + ImagePolicy for docs-site image automation' (#86) from feature/issue-85-docs-site-image-automation into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 46s
Validate Flux manifests / kustomize-build (push) Failing after 17s
2026-05-29 00:02:55 +00:00
agent-company 459fc99f31 feat(flux): add docs-site ImageRepository, ImagePolicy, and setter marker
Validate Flux manifests / kustomize-build (pull_request) Failing after 51s
Closes leeworks-agents/api-company#85

- Add ImageRepository 'docs-site' in flux-system watching
  registry.leeworks.dev/leeworks-agents/docs-site (5m poll interval)
- Add ImagePolicy 'docs-site' in flux-system using alphabetical/asc
  ordering (consistent with SHA-tag CI pushes)
- Add $imagepolicy setter marker comment to flux/docs-site/helmrelease.yaml
  so Flux ImageUpdateAutomation can auto-commit new image tags

kustomize build flux/ = PASS
2026-05-29 00:02:23 +00:00
AI-Manager e470826a78 Merge pull request 'chore: update STATUS.md — 2026-05-28 cycle-2 triage' (#84) from feature/status-update-2026-05-28-cycle2 into main
Validate Flux manifests / kustomize-build (push) Failing after 21s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 47s
2026-05-28 20:02:23 +00:00
agent-company c5947359c5 chore: update STATUS.md — 2026-05-28 cycle-2 triage
Validate Flux manifests / kustomize-build (pull_request) Failing after 26s
2026-05-28 20:02:12 +00:00
AI-Manager 5555c7524b Merge pull request 'chore: STATUS.md update — 2026-05-28 late-night triage' (#82) from feature/status-update-2026-05-28-night into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 43s
Validate Flux manifests / kustomize-build (push) Failing after 21s
2026-05-28 15:03:43 +00:00
agent-company 8c46feb1c6 chore: update STATUS.md for 2026-05-28 late-night triage cycle
Validate Flux manifests / kustomize-build (pull_request) Failing after 25s
Triaged issues #79, #80, #81 (all manual operator secret tasks).
No open PRs. kustomize build flux/ = PASS.
2026-05-28 15:03:20 +00:00
AI-Manager 28878773dd Merge pull request 'chore: update STATUS.md — 2026-05-28 night triage cycle' (#78) from feature/status-update-2026-05-28-night into main
Validate Flux manifests / kustomize-build (push) Failing after 16s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 36s
2026-05-28 10:02:47 +00:00
agent-company c71651d4a2 chore: update STATUS.md — 2026-05-28 night triage cycle
Validate Flux manifests / kustomize-build (pull_request) Failing after 34s
Triaged issues #76 and #77 (new operator secret tasks for Flux GitRepository
auth and gitea-act-runner token). Both are blocked manual operator tasks;
instructions posted on each issue. No PRs open. kustomize build flux/ = PASS.
2026-05-28 10:02:30 +00:00
AI-Manager 636d68ced5 Merge pull request 'chore: update STATUS.md — 2026-05-28 evening triage cycle' (#75) from feature/status-update-2026-05-28-evening into main
Validate Flux manifests / kustomize-build (push) Failing after 21s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 45s
2026-05-28 05:03:12 +00:00
agent-company b62bf046f6 chore: update STATUS.md — 2026-05-28 evening triage cycle
Validate Flux manifests / kustomize-build (pull_request) Failing after 21s
2026-05-28 05:02:57 +00:00
AI-Manager 181b9f9501 Merge pull request 'chore: update STATUS.md — 2026-05-28 morning triage cycle' (#72) from feature/status-update-2026-05-28-morning into main
Validate Flux manifests / kustomize-build (push) Failing after 24s
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 2m42s
2026-05-28 00:04:55 +00:00
agent-company d465c26832 chore: update STATUS.md — 2026-05-28 morning triage cycle
Validate Flux manifests / kustomize-build (pull_request) Failing after 21s
2026-05-28 00:04:41 +00:00
AI-Manager 49af1e73a6 Merge pull request 'ci: add Gitea Actions workflow to validate kustomize build on every PR (issue #69)' (#71) from feature/issue-69-gitea-actions-ci into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 48s
Validate Flux manifests / kustomize-build (push) Failing after 17s
2026-05-28 00:02:16 +00:00
agent-company d9a13d8c29 ci: add Gitea Actions workflow to validate kustomize build on every PR (Closes leeworks-agents/api-company#69)
Validate Flux manifests / kustomize-build (pull_request) Failing after 46s
2026-05-28 00:01:52 +00:00
AI-Manager 04102c75a4 Merge pull request 'chore: update STATUS.md — 2026-05-27 night triage cycle' (#68) from feature/status-update-2026-05-27-night into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 3m42s
2026-05-27 20:05:47 +00:00
agent-company 3860b44dc5 chore: update STATUS.md — 2026-05-27 night triage cycle
Triaged 2 new issues:
- #67: grafana.leeworks.dev DNS checklist — already covered in runbook (no code change)
- #66: Flux validation checklist — blocked on operator Phase 0 actions

kustomize build flux/ = PASS
2026-05-27 20:05:27 +00:00
AI-Manager 94631c18d0 Merge pull request 'feat: deploy External Secrets Operator (ESO) via Flux (issue #61)' (#65) from feature/issue-61-eso-flux into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 43s
2026-05-27 15:07:40 +00:00
AI-Manager c394c0c563 Merge pull request 'docs: add rapidapi-proxy-secret Step 10 to secrets-checklist (issue #60)' (#64) from feature/issue-60-rapidapi-proxy-secret-checklist into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 44s
Merge PR #64
2026-05-27 15:07:06 +00:00
AI-Manager 886d8e73d3 Merge pull request 'docs: add grafana.leeworks.dev as 7th DNS record (issue #62)' (#63) from feature/issue-62-dns-grafana into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 51s
2026-05-27 15:06:45 +00:00
agent-company 43f5d5ef60 feat: deploy External Secrets Operator (ESO) via Flux + real ExternalSecrets
Add flux/external-secrets/ directory containing:
- namespace.yaml: external-secrets namespace
- helmrepository.yaml: charts.external-secrets.io HelmRepository
- helmrelease.yaml: ESO HelmRelease (external-secrets/external-secrets >=0.9.0 <1.0.0)
- clustersecretstore.yaml: ClusterSecretStore using Kubernetes provider
  (ServiceAccount + ClusterRole + ClusterRoleBinding + ClusterSecretStore)
- kustomization.yaml: wires all above resources

Uncomment and complete ExternalSecret manifests in each API namespace:
- flux/zip-enrichment/externalsecret.yaml
- flux/holidays/externalsecret.yaml
- flux/air-quality/externalsecret.yaml

All three ExternalSecrets reference the kubernetes-provider ClusterSecretStore
and will auto-sync rapidapi-proxy-secret once ESO is running.

Add external-secrets to flux/kustomization.yaml.

kustomize build flux/ = PASS

Closes leeworks-agents/api-company#61
2026-05-27 15:05:54 +00:00
agent-company a0620ea391 docs: add Step 10 rapidapi-proxy-secret to secrets-checklist.md
Add checklist item 10 and full detail section for the rapidapi-proxy-secret
Kubernetes secret that must be created in each API namespace (zip-enrichment,
holidays, air-quality) before the Phase 3 server middleware can validate
incoming RapidAPI requests.

Includes:
- Checklist item 10 in the summary list
- Full detail section with kubectl commands for all 3 namespaces
- Note about placeholder ExternalSecret manifests and ESO (issue #61)
- Updated dependency-order diagram

Closes leeworks-agents/api-company#60
2026-05-27 15:04:45 +00:00
agent-company 8902feada7 docs: add grafana.leeworks.dev as 7th DNS record in dns.md and operator-runbook
- dns.md: fix '6 A records' wording to '7 A records'
- dns.md: add grafana.leeworks.dev to status checklist and dig verification block
- operator-runbook.md: fix 'all six' to 'all seven', add registry.leeworks.dev
  row to Phase 4 DNS table, add dig verification loop

Closes leeworks-agents/api-company#62
2026-05-27 15:04:13 +00:00
AI-Manager 13ce96e07e Merge pull request 'docs: add secrets-checklist items 8 & 9 (gitea-registry + gitea-image-automation-token)' (#59) from feature/secrets-checklist-items-8-9 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 55s
2026-05-27 10:03:09 +00:00
agent-company fe9d867a87 docs: add secrets checklist items 8 and 9 for gitea-registry and gitea-image-automation-token
- Item 8: gitea-registry imagePullSecret in zip-enrichment, holidays,
  air-quality, docs-site namespaces (closes leeworks-agents/api-company#58)
- Item 9: gitea-image-automation-token in flux-system with write:repository
  scope for Flux ImageUpdateAutomation (closes leeworks-agents/api-company#57)
- Wire gitea-image-automation-token as push.secretRef in imageupdateautomation.yaml
- Update dependency order and item count from seven to nine
2026-05-27 10:02:42 +00:00
AI-Manager 9ad73f3741 Merge pull request 'chore: update STATUS.md — 2026-05-27 evening triage cycle' (#56) from feature/status-update-2026-05-27-evening into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 56s
2026-05-27 05:03:29 +00:00
agent-company e72a845c31 chore: update STATUS.md — 2026-05-27 evening triage cycle 2026-05-27 05:03:09 +00:00
AI-Manager b371c39c13 Merge pull request 'chore: update STATUS.md — 2026-05-27 ship cycle' (#55) from feature/status-update-2026-05-27-ship into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 54s
2026-05-27 00:42:22 +00:00
agent-company 33c6007899 chore: update STATUS.md — 2026-05-27 ship cycle 2026-05-27 00:41:58 +00:00
AI-Manager 9a1639ff4f Merge pull request 'chore: update STATUS.md — 2026-05-27 morning agent cycle' (#54) from feature/status-update-2026-05-27 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 50s
2026-05-27 00:05:46 +00:00
agent-company 4ab65ca4dd chore: update STATUS.md — 2026-05-27 morning agent cycle 2026-05-27 00:05:28 +00:00
AI-Manager 7311df25bc Merge pull request 'chore: update STATUS.md — 2026-05-26 evening agent cycle' (#53) from feature/status-update-2026-05-26c into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 55s
2026-05-26 20:46:53 +00:00
agent-company 53466186b8 chore: update STATUS.md — 2026-05-26 evening agent cycle 2026-05-26 20:46:35 +00:00
AI-Manager 8d5ae5ee31 Merge pull request 'docs: add operator-runbook.md — ordered manual-action guide (closes #50)' (#52) from feature/operator-runbook-50 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 57s
2026-05-26 20:46:01 +00:00
agent-company 82c9f70a01 feat: add Flux ImageRepository + ImagePolicy + ImageUpdateAutomation for all three API services (closes leeworks-agents/api-company#51) 2026-05-26 20:45:33 +00:00
agent-company 68a1524dca docs: add operator-runbook.md — ordered manual-action guide for operator (closes leeworks-agents/api-company#50) 2026-05-26 20:45:33 +00:00
AI-Manager bf19fb1cf5 Merge pull request 'chore: update STATUS.md — 2026-05-26 PM agent cycle' (#49) from feature/status-update-2026-05-26b into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 53s
2026-05-26 20:05:03 +00:00
agent-company 54cd793d5e chore: update STATUS.md — 2026-05-26 PM agent cycle 2026-05-26 20:04:40 +00:00
AI-Manager 58eba5f342 Merge pull request 'feat: scaffold Flux manifests for zip-enrichment, holidays, air-quality API services (closes #46)' (#48) from feature/flux-api-services-46 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 49s
2026-05-26 20:03:47 +00:00
agent-company ae978a0bb3 feat: scaffold Flux manifests for zip-enrichment, holidays, air-quality API services
Closes leeworks-agents/api-company#46

Add flux/ directories for all three API services, each containing:
- namespace.yaml — Kubernetes Namespace
- helmrelease.yaml — HelmRelease using bedag/raw chart with Deployment,
  Service, and Ingress (TLS via cert-manager letsencrypt-prod)
- externalsecret.yaml — placeholder/comment for RapidAPI proxy-secret;
  includes kubectl manual-create instructions until ESO is deployed
- kustomization.yaml — per-directory Kustomize resources list

Services and subdomains:
  zip-enrichment  → zip.leeworks.dev
  holidays        → holidays.leeworks.dev
  air-quality     → aqi.leeworks.dev

Images:
  registry.leeworks.dev/zip-enrichment/server:latest
  registry.leeworks.dev/holidays/server:latest
  registry.leeworks.dev/air-quality/server:latest

Update root flux/kustomization.yaml to include all three new directories.
kustomize build flux/ passes with zero errors.
2026-05-26 20:02:42 +00:00
AI-Manager 01bfe2693a Merge pull request 'docs: add secrets-checklist.md — all required Kubernetes secrets with kubectl commands (closes #43)' (#45) from feature/secrets-checklist-43 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 56s
2026-05-26 15:03:31 +00:00
agent-company 091c5cd089 docs: add secrets-checklist.md with kubectl commands for all required secrets
Closes leeworks-agents/api-company#43

Creates docs/secrets-checklist.md containing:
- Human operator checklist (7 checkboxes)
- Exact kubectl create secret commands for all required secrets
- Source of each secret value (Gitea admin UI, Slack, etc.)
- Which issues are unblocked by each secret
- Dependency order diagram

Secrets covered:
1. gitea-leeworks-agents-token (flux-system) → unblocks Flux GitRepo auth (#2)
2. gitea-runner-token (gitea-runner) → unblocks act-runner (#3)
3. grafana-admin (monitoring) → unblocks Grafana (#7)
4. gatus-slack-webhook (monitoring) → unblocks Gatus alerts (#8)
5. GITEA_TOKEN Actions secret (per repo) → unblocks CI image push
6. Gitea packages enable + DNS → unblocks registry (#4)
7. Flux source in Talos repo → unblocks GitOps (#2)
2026-05-26 15:03:05 +00:00
AI-Manager 997bf7bb12 Merge pull request 'chore: update STATUS.md — 2026-05-26 agent cycle' (#42) from feature/status-triage-2026-05-26 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 51s
2026-05-26 10:08:16 +00:00
agent-company f7aef173ff chore: update STATUS.md — 2026-05-26 agent cycle triage 2026-05-26 10:08:04 +00:00
AI-Manager c899450f56 Merge pull request 'docs: cluster audit — nodes, namespaces, ingress, Flux state (closes #26)' (#34) from feature/cluster-audit-and-triage into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 43s
2026-05-25 10:04:10 +00:00
agent-company 5e7fbc5bfd docs: add cluster-audit.md with nodes/namespaces/ingress/Flux state
Covers all required sections per issue acceptance criteria:
- Nodes (3x Talos control plane, 10.0.1.3/4/5)
- Namespaces (active + staged + future)
- Ingress rules (all 8 leeworks.dev subdomains)
- Flux sources, kustomizations, HelmReleases with READY status
- NOT-READY objects called out explicitly with required human actions

Note: kubectl unavailable from agent container; data sourced from
STATUS.md, flux/ manifests, and prior session logs. Human operator
should verify live state.

Closes leeworks-agents/api-company#26
2026-05-25 10:03:55 +00:00
AI-Manager d110f14922 Merge pull request 'feat: Grafana dashboard ConfigMap, PrometheusRule SLO alerts, RapidAPI listing copy' (#25) from feature/monitoring-dashboards-alerts-listings into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 34s
2026-05-25 05:04:52 +00:00
agent-company c748e8d074 feat: Grafana dashboard ConfigMap, PrometheusRule SLO alerts, RapidAPI listing copy
- flux/monitoring/grafana-dashboard-apis.yaml: Grafana dashboard ConfigMap
  labelled grafana_dashboard=1, covering request rate, P50/P95/P99 latency,
  5xx error rate, and data freshness for all three APIs.
  Closes leeworks-agents/api-company#22

- flux/monitoring/prometheusrule-apis.yaml: PrometheusRule with four alert rules:
  APIHighErrorRate (warning >5%, critical >20%), APIHighLatency (P95 >2s),
  APIDataStale (>30 days), APIDown (up==0 for 2 min). All labelled
  severity + team: api-company.
  Closes leeworks-agents/api-company#23

- docs/rapidapi-listings.md: Full RapidAPI listing copy for zip-enrichment,
  holidays, and air-quality — API name, tagline (<=120 chars), short description
  (<=300 chars), long description, category, plan table (Basic $9/Pro $19/
  Ultra $49), endpoint descriptions, and keywords.
  Closes leeworks-agents/api-company#24

- flux/monitoring/kustomization.yaml: added grafana-dashboard-apis.yaml
  and prometheusrule-apis.yaml to resources list.
2026-05-25 05:04:28 +00:00
AI-Manager e738da6448 Merge pull request 'chore: update STATUS.md with 2026-05-25 agent cycle status' (#21) from feature/status-update-2026-05-25 into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 39s
2026-05-25 00:05:31 +00:00
agent-company d35fcf7849 chore: update STATUS.md with 2026-05-25 agent cycle status
- Mark zip-enrichment and holidays as in-progress for spec+code
- Document all Flux manifests as kustomize-build passing
- List 6 concrete human operator actions needed to unblock infra
- Reference all open issues with current state
- Update API repos progress (Phase 1-3 in-flight)
2026-05-25 00:05:15 +00:00
AI-Manager ba469c6fa5 Merge pull request 'feat: docs-site scaffold, legal docs, metrics standard, Flux manifests (closes #5 #9 #10 #11 #12 #13 #14)' (#20) from feature/docs-legal-metrics-registry into main
Build Docs Site / Aggregate OpenAPI Specs (push) Failing after 2m51s
2026-05-24 23:21:10 +00:00
agent-company a615b7ebfd feat: implement docs-site, legal docs, metrics standard, flux manifests
Closes leeworks-agents/api-company#5  (docs-site Astro scaffold)
Closes leeworks-agents/api-company#9  (metrics instrumentation standard)
Closes leeworks-agents/api-company#10 (Gitea Actions openapi aggregation pipeline)
Closes leeworks-agents/api-company#11 (docs-site Flux HelmRelease)
Closes leeworks-agents/api-company#12 (SEO blog posts x3)
Closes leeworks-agents/api-company#13 (legal docs ToS/Privacy/AUP)
Closes leeworks-agents/api-company#14 (DNS documentation)

## Changes

### docs/legal/
- terms-of-service.md — API usage, liability, account termination, governing law
- privacy-policy.md — request log retention (90d), no PII sold, data sharing
- acceptable-use-policy.md — rate limit abuse, scraping prohibition, resale ban

### docs/metrics-standard.md
- Defines api_requests_total, api_response_duration_seconds, api_data_freshness_seconds
- Fastify (TypeScript) and FastAPI (Python) reference middleware implementations
- Prometheus scrape config and Grafana dashboard guidance

### docs/registry.md
- Decision: use Gitea built-in container registry (no new infra)
- Image naming convention, auth, Kubernetes imagePullSecrets, ingress config

### docs/dns.md
- Required A records for all 6 subdomains
- cert-manager ClusterIssuer and Ingress TLS examples
- Verification commands and human-operator action items

### docs-site/
- Astro 4 + MDX + sitemap scaffold
- Base layout with nav linking all APIs, blog, RapidAPI, status
- Landing page with API cards
- Per-API Redoc viewer pages (zip-enrichment, holidays, air-quality)
- Blog index + 3 SEO blog posts (~1000 words each with JSON-LD)
- Dockerfile (multi-stage: node build + nginx serve)
- nginx.conf with gzip, caching, health endpoint

### flux/
- gitea-runner/: gitea-act-runner HelmRelease (org-scope, dind)
- monitoring/: kube-prometheus-stack + Gatus HelmReleases
  - Prometheus with pod annotation scraping
  - Grafana at grafana.leeworks.dev with persistence
  - Gatus status page at status.leeworks.dev, 90-day retention
- docs-site/: Deployment + Service + Ingress via raw chart
- api-company-source/: GitRepository + Kustomization reference manifests
- kustomization.yaml: root kustomize entry point (build validated)

### .gitea/workflows/build-docs.yaml
- Aggregates openapi.yaml from zip-enrichment, holidays, air-quality repos
- Builds Astro docs-site
- Pushes image to registry.leeworks.dev/leeworks-agents/docs-site
- Triggered on push to main, schedule daily 02:00 UTC, workflow_dispatch
2026-05-24 23:20:33 +00:00
79 changed files with 4895 additions and 17 deletions
+85
View File
@@ -0,0 +1,85 @@
# Gitea Actions: Aggregate openapi.yaml specs + trigger docs-site build
# Closes leeworks-agents/api-company#10
name: Build Docs Site
on:
push:
branches: [main]
workflow_dispatch:
schedule:
# Re-build daily at 02:00 UTC to pick up spec changes
- cron: '0 2 * * *'
jobs:
aggregate-specs:
name: Aggregate OpenAPI Specs
runs-on: ubuntu-latest
steps:
- name: Checkout api-company
uses: actions/checkout@v4
with:
path: api-company
- name: Checkout zip-enrichment
uses: actions/checkout@v4
with:
repository: leeworks-agents/zip-enrichment
token: ${{ secrets.GITEA_TOKEN }}
path: zip-enrichment
- name: Checkout holidays
uses: actions/checkout@v4
with:
repository: leeworks-agents/holidays
token: ${{ secrets.GITEA_TOKEN }}
path: holidays
- name: Checkout air-quality
uses: actions/checkout@v4
with:
repository: leeworks-agents/air-quality
token: ${{ secrets.GITEA_TOKEN }}
path: air-quality
- name: Copy openapi.yaml specs into docs-site
run: |
mkdir -p api-company/docs-site/public/specs
cp zip-enrichment/openapi.yaml api-company/docs-site/public/specs/zip-enrichment.yaml
cp holidays/openapi.yaml api-company/docs-site/public/specs/holidays.yaml
cp air-quality/openapi.yaml api-company/docs-site/public/specs/air-quality.yaml
echo "Specs copied:"
ls -la api-company/docs-site/public/specs/
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install docs-site dependencies
working-directory: api-company/docs-site
run: npm ci
- name: Build docs-site
working-directory: api-company/docs-site
run: npm run build
- name: Log in to container registry
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login registry.leeworks.dev \
-u ${{ gitea.actor }} --password-stdin
- name: Build and push docs-site image
working-directory: api-company/docs-site
run: |
IMAGE="registry.leeworks.dev/leeworks-agents/docs-site"
SHA="${{ gitea.sha }}"
docker build -t "$IMAGE:$SHA" -t "$IMAGE:latest" .
docker push "$IMAGE:$SHA"
docker push "$IMAGE:latest"
echo "Pushed $IMAGE:$SHA"
- name: Trigger Flux reconcile (optional)
run: |
echo "Image pushed. Flux will detect new tag via image automation and re-deploy docs-site."
echo "If image automation is not configured, manually run: flux reconcile helmrelease docs-site -n docs-site"
+58
View File
@@ -0,0 +1,58 @@
# Validate and publish OpenAPI specs to RapidAPI when apis/*/openapi.yaml changes on main.
# Requires secrets (configured once RapidAPI listings are live):
# RAPIDAPI_PLATFORM_KEY, RAPIDAPI_ZIP_API_ID, RAPIDAPI_ZIP_VERSION_ID,
# RAPIDAPI_HOLIDAYS_API_ID, RAPIDAPI_HOLIDAYS_VERSION_ID,
# RAPIDAPI_AQI_API_ID, RAPIDAPI_AQI_VERSION_ID
#
# If RAPIDAPI_PLATFORM_KEY is not set the publish step exits 0 with a skip message.
name: Validate and Publish OpenAPI Specs
on:
push:
branches:
- main
paths:
- 'apis/*/openapi.yaml'
jobs:
publish:
name: Lint and publish specs
runs-on: ubuntu-latest
steps:
- name: Checkout (with history for diff)
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Redocly CLI
run: npm install -g @redocly/cli@latest
- name: Lint OpenAPI specs
run: |
echo "Linting all OpenAPI specs..."
npx @redocly/cli lint apis/*/openapi.yaml
- name: Detect changed specs
id: changed
run: |
changed=$(git diff --name-only HEAD~1 HEAD | grep 'openapi\.yaml' || true)
echo "Changed specs: ${changed:-none}"
echo "files=${changed}" >> "$GITHUB_OUTPUT"
- name: Publish specs to RapidAPI
env:
RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_PLATFORM_KEY }}
RAPIDAPI_ZIP_API_ID: ${{ secrets.RAPIDAPI_ZIP_API_ID }}
RAPIDAPI_ZIP_VERSION_ID: ${{ secrets.RAPIDAPI_ZIP_VERSION_ID }}
RAPIDAPI_HOLIDAYS_API_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_API_ID }}
RAPIDAPI_HOLIDAYS_VERSION_ID: ${{ secrets.RAPIDAPI_HOLIDAYS_VERSION_ID }}
RAPIDAPI_AQI_API_ID: ${{ secrets.RAPIDAPI_AQI_API_ID }}
RAPIDAPI_AQI_VERSION_ID: ${{ secrets.RAPIDAPI_AQI_VERSION_ID }}
run: node scripts/publish-openapi.js
+21
View File
@@ -0,0 +1,21 @@
# Smoke test: confirms Gitea Act Runner is online and accepting jobs.
# Run manually via workflow_dispatch after runner is registered (#77).
# Closes leeworks-agents/api-company#96
name: Runner Smoke Test
on:
workflow_dispatch:
jobs:
smoke:
name: Smoke Test
runs-on: ubuntu-latest
steps:
- name: Confirm runner is online
run: |
echo "Runner is online!"
echo "Job ID: $GITHUB_JOB"
echo "Runner OS: $(uname -a)"
echo "Date: $(date -u)"
echo "Smoke test PASSED"
+21
View File
@@ -0,0 +1,21 @@
name: Validate Flux manifests
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
kustomize-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kustomize
run: |
curl -sL "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
- name: kustomize build flux/
run: kustomize build flux/ > /dev/null
+131 -14
View File
@@ -1,26 +1,143 @@
# Company Status
_Last updated: 2026-05-18 (bootstrap)_
_Last updated: 2026-05-30 (sprint planning cycle)_
## APIs
| API | Spec | Code | Deployed | Listed on RapidAPI | Paying Users | MRR |
|----------------|------|------|----------|--------------------|--------------|-----|
| ZIP Enrichment | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
| Holidays | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
| ZIP Enrichment | [~] | [~] | [ ] | [ ] | 0 | $0 |
| Holidays | [~] | [~] | [ ] | [ ] | 0 | $0 |
| Air Quality | [ ] | [ ] | [ ] | [ ] | 0 | $0 |
Legend: [x]=done, [~]=in-progress, [ ]=not started
## Infrastructure
- Cluster nodes: 3 control plane (10.0.1.3, .4, .5) + workers (testing1)
- Flux healthy: TBD — see Phase-0 issue #1
- Gitea Actions runner: DOWN (Phase-0 issue #3)
- VPS tunnel: TBD
- Container registry: NOT DEPLOYED (Phase-0 issue #4)
- **Flux wiring (api-company):** Manifests staged at `flux/api-company-source/` — PENDING Talos merge (issue #2)
- **Gitea Actions runner:** Flux manifest committed at `flux/gitea-runner/` — PENDING runner token secret + Talos wiring (issue #3)
- **Container registry:** Gitea built-in registry selected; docs/registry.md committed — PENDING Gitea packages enabled (issue #4)
- **Prometheus + Grafana:** Flux HelmRelease at `flux/monitoring/` — PENDING Flux wiring + Grafana secret (issue #7)
- **Gatus status page:** Flux HelmRelease at `flux/monitoring/gatus-helmrelease.yaml` — PENDING Flux wiring (issue #8)
- **API service manifests:** `flux/zip-enrichment/`, `flux/holidays/`, `flux/air-quality/` scaffolded (PR #48, closes #46)
- **Image automation:** `flux/image-automation/` — ImageRepository + ImagePolicy + ImageUpdateAutomation for all three APIs
## Revenue
- Gross MRR: $0
- Net MRR (after ~26.5% fees): $0
- Target: $100/mo net
- Gap: $100
## Completed This Cycle (2026-05-30 — Sprint Cycle)
- **#108** — STATUS.md updated with 2026-05-30 sprint plan and critical path.
- **#107** — docs-site build verified: `npm run build` exits 0; all 9 required routes present in `dist/` (`/`, `/zip-enrichment`, `/holidays`, `/air-quality`, `/blog/` (3 posts), `/legal/terms-of-service`, `/legal/privacy-policy`, `/legal/acceptable-use-policy`, `/pricing`). Unblocks #30.
- **PR #105 MERGED** — `scripts/publish-openapi.js` + `.gitea/workflows/publish-openapi.yaml` (closes #100).
- **PR #103 MERGED** — `docs-site/src/pages/pricing.astro` + nav link (closes #101).
- **PR #104 MERGED** — `research/RESEARCH_LOG.md` first session: VIN Decoder next candidate (closes #102).
- **New issues triaged** — #100#108 (sprint planning cycle issues reviewed).
- **`0xWheatyz/api-company`** — still does not exist; fork sync skipped (blocker #47).
- **`kustomize build flux/` = PASS** — no regressions.
## Next action
Phase-0 cluster audit (issue #1 in this repo). Until Flux is confirmed watching this org and the runner+registry are up, no API CI can land.
## Critical Path (operator must complete in order)
1. **Create `0xWheatyz/api-company` repo** (#47) — **highest priority, unblocks everything**
2. **Add api-company Flux source to Talos** (#90) — needs Talos PR, reference manifests at `flux/api-company-source/`
3. **Create `gitea-leeworks-agents-token` secret** in `flux-system` (#76)
4. **Create `gitea-runner-token` secret** in `gitea-runner` (#77)
5. **Enable Gitea packages + DNS for `registry.leeworks.dev`** (#4)
6. **All other secrets** (#70, #73, #74, #79, #80, #81, #83) follow in order
## New Issues This Sprint (2026-05-29, #100#108)
- **#100** — `scripts/publish-openapi.js` + CI workflow — **DONE (PR #105 merged)**
- **#101** — Pricing page at `docs.leeworks.dev/pricing/`**DONE (PR #103 merged)**
- **#102** — Seed `research/RESEARCH_LOG.md`**DONE (PR #104 merged)**
- **#106** — Add `grafana.leeworks.dev` as 7th subdomain to DNS task (#33) — tracker only; docs already updated (#67)
- **#107** — docs-site build verification — **DONE (build passes, all 9 routes confirmed)**
- **#108** — STATUS.md update — **DONE (this update)**
## Backlog
- **28 open issues total**; all remaining blocked on operator Phase 0 actions above.
- **Agent work that can proceed without operator:** all completed this cycle.
## Completed Previous Cycle (2026-05-28 — Cycle-2 Triage)
- **#83** — Triaged: `GITEA_TOKEN` Actions secrets needed in `zip-enrichment`, `holidays`, `air-quality` repos. Manual operator task; reuse token from #74 if it has `write:package` scope. Step-by-step instructions posted.
- **No open PRs** — queue empty.
- **`kustomize build flux/` = PASS** — no regressions.
- **Upstream `0xWheatyz/api-company`** — still does not exist; fork sync skipped (blocker #47).
## Completed Previous Cycle (2026-05-28 — Late-Night Triage)
- **#79** — Triaged: `gitea-registry` imagePullSecret needed in 4 namespaces. Manual operator task; kubectl docker-registry secret instructions posted.
- **#80** — Triaged: `gitea-image-automation-token` secret needed in `flux-system`. Manual operator task; write-scoped PAT required, kubectl instructions posted.
- **#81** — Triaged: `rapidapi-proxy-secret` needed in `zip-enrichment`, `holidays`, `air-quality`. Manual operator task; placeholder + real-secret commands posted.
- **No open PRs** — queue empty.
- **`kustomize build flux/` = PASS** — no regressions.
## Completed Previous Cycle (2026-05-28 — Night Triage)
- **#76** — Triaged: `gitea-leeworks-agents-token` secret required in `flux-system` to unblock Flux GitRepository auth. Manual operator task; kubectl instructions + reconcile steps posted in issue comment.
- **#77** — Triaged: `gitea-runner-token` secret required in `gitea-runner` namespace to register Act Runner. Manual operator task; full step-by-step instructions posted in issue comment.
- **No open PRs** — queue empty.
- **`kustomize build flux/` = PASS** — no regressions.
## Completed Previous Cycle (2026-05-28 — Evening Triage)
- **#74** — Triaged: `GITEA_TOKEN` Actions secret required in leeworks-agents/api-company for docs-site CI. Manual operator task; instructions posted.
- **#73** — Triaged: `gatus-slack-webhook` secret required in `monitoring` namespace. Manual operator task; kubectl instructions posted.
- **#67** — Confirmed already resolved: `docs/operator-runbook.md` and `docs/dns.md` both list all 7 subdomains incl. `grafana.leeworks.dev` (PR #63). No code change needed.
- **No open PRs** — queue empty.
- **`kustomize build flux/` = PASS** — no regressions.
## Completed Previous Cycle (2026-05-28 — Morning Triage)
- **#69 → PR #71 MERGED** — Added `.gitea/workflows/validate-flux.yaml` CI workflow: runs `kustomize build flux/` on every PR and push to `main`. CI gate enforced once Act Runner (#3) is online.
- **#70** — Triaged: `grafana-admin` secret needed in `monitoring` namespace. Manual operator task; kubectl instructions posted in issue comment.
- **#67** — Confirmed `docs/operator-runbook.md` Phase 4 DNS table already lists all 7 subdomains including grafana.leeworks.dev. No code change needed.
- **#66** — Blocked on Phase 0 operator actions (#47, #2, #3, #4). Status comment posted.
- **#47, #2, #3, #4, #7, #8, #16, #17, #18, #19, #27, #30, #33, #44** — Status comments posted confirming blocked/awaiting-operator state.
- **No open PRs to review** — queue empty after PR #71 merged.
- **`kustomize build flux/` = PASS** — no regressions.
## Completed Previous Cycle (2026-05-27 — Evening Triage)
- **Triage pass** — all 15 open agent-ready issues reviewed; status comments posted on each.
- **No open PRs** — nothing to merge or review.
- **No new feature work** — all issues remain blocked on operator actions or cross-repo prerequisites.
- **`kustomize build flux/` = PASS** — no regressions.
## Completed Previous Cycle (2026-05-27 — Ship/Afternoon)
- **PRs reviewed** — 0 open PRs on fork; nothing to merge.
- **Upstream sync** — `0xWheatyz/api-company` does not yet exist (blocker #41/#47); `git fetch upstream` failed as expected. Fork `origin/main` is current (SHA `9a1639f`).
- **`kustomize build flux/` = PASS** — all sub-trees validate cleanly; no regressions.
- **Deployment PR** — skipped; upstream repo must be created by operator first (#41, #47).
- **No new feature work** — all 15 open agent-ready issues remain blocked on operator actions.
## Completed Previous Cycle (2026-05-27 — Morning)
- **Triage pass** — all 15 open agent-ready issues reviewed; status comments posted/confirmed on each.
- **#19** — First triage comment posted: RapidAPI/PayPal operator task fully documented; all agent-side prerequisites (OpenAPI specs, listing copy, proxy-secret placeholders) confirmed ready.
- **No PRs to review** — fork had no open PRs.
- **No new feature work** — all open issues blocked on operator actions.
## Completed Previous Cycle (2026-05-26 Evening)
- **#50** — `docs/operator-runbook.md` added: ordered phase-by-phase manual for operator. PR #52 merged.
- **#51** — Flux image automation: `ImageRepository` + `ImagePolicy` + `ImageUpdateAutomation` for all three API services; setter markers added to HelmReleases. PR #52 merged. `kustomize build flux/ = PASS`.
## Completed Previous Cycle (2026-05-26 PM)
- **#46** — Scaffolded Flux deployment manifests for all three API services (zip-enrichment, holidays, air-quality). PR #48 merged.
## Completed Previous Cycle (2026-05-26 AM)
- **#43** — secrets-checklist.md added (closed, PR #45)
- **#36** — Cluster audit committed to `docs/cluster-audit.md` (closed)
- **#40** — Legal docs (ToS, Privacy Policy, AUP) under `docs/legal/` (closed)
- **#37** — docs-site Astro skeleton with Redoc pages; `npm run build` passes (closed)
- **#39** — SEO blog posts (ZIP, Holidays, Air Quality) in `docs-site/src/pages/blog/` (closed)
- **#38** — Gitea Actions CI workflow (`.gitea/workflows/build-docs.yaml`) + Dockerfile (closed)
- **#34** — Cluster audit PR merged
## Flux Manifests (kustomize build flux/ = PASS)
All flux manifests validate successfully. Committed components pending Flux activation:
- `gitea-runner` namespace + HelmRelease (gitea-act-runner chart)
- `monitoring` namespace + kube-prometheus-stack HelmRelease
- `monitoring` Gatus HelmRelease (status.leeworks.dev, 90-day retention)
- `docs-site` HelmRelease (docs.leeworks.dev)
- `zip-enrichment` namespace + HelmRelease (zip.leeworks.dev) + rapidapi-proxy-secret placeholder
- `holidays` namespace + HelmRelease (holidays.leeworks.dev) + rapidapi-proxy-secret placeholder
- `air-quality` namespace + HelmRelease (aqi.leeworks.dev) + rapidapi-proxy-secret placeholder
- `image-automation` ImageRepository + ImagePolicy + ImageUpdateAutomation for all three APIs
## Blockers (human operator action required)
1. **Create `0xWheatyz/api-company` repo on Gitea** — every ship cycle fails until this exists (#41, #47) **<-- DO THIS FIRST**
2. **Add api-company GitRepository+Kustomization to 0xWheatyz/Talos** at `testing1/first-cluster/cluster/flux/` — reference manifests ready in `flux/api-company-source/` (#2)
3. **Create `gitea-leeworks-agents-token` secret** in `flux-system` namespace (HTTPS token for Gitea)
4. **Create `gitea-runner-token` secret** in `gitea-runner` namespace (Gitea Admin -> Actions -> Runners -> New Runner) (#3)
5. **Enable Gitea packages** (for container registry at `registry.leeworks.dev`) (#4)
6. **RapidAPI + PayPal setup** — manual, gated on operator turning 18 (#19, #44)
> Full ordered runbook with copy-paste commands: `docs/operator-runbook.md`
-1
View File
@@ -1 +0,0 @@
# placeholder — populated by Phase-4/5 issues
+14
View File
@@ -0,0 +1,14 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://docs.leeworks.dev',
integrations: [mdx(), sitemap()],
output: 'static',
});
+25
View File
@@ -0,0 +1,25 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|svg|ico|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check
location /health {
return 200 "ok\n";
add_header Content-Type text/plain;
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "leeworks-docs-site",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^4.8.0",
"@astrojs/mdx": "^3.0.0",
"@astrojs/sitemap": "^3.1.0",
"redoc": "^2.1.5"
},
"devDependencies": {
"typescript": "^5.4.0"
}
}
+51
View File
@@ -0,0 +1,51 @@
---
export interface Props {
title: string;
description?: string;
}
const { title, description = "leeworks.dev API documentation" } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content={description} />
<title>{title} | leeworks.dev APIs</title>
<link rel="sitemap" href="/sitemap-index.xml" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: #0f1117; color: #e2e8f0; }
nav { background: #1a1d27; border-bottom: 1px solid #2d3748; padding: 0 2rem; display: flex; align-items: center; gap: 2rem; height: 60px; }
nav a { color: #90cdf4; text-decoration: none; font-weight: 500; }
nav a:hover { color: #fff; }
nav .brand { font-size: 1.25rem; font-weight: 700; color: #fff; }
main { min-height: calc(100vh - 120px); }
footer { background: #1a1d27; border-top: 1px solid #2d3748; padding: 1.5rem 2rem; text-align: center; color: #718096; font-size: 0.875rem; }
footer a { color: #90cdf4; }
</style>
</head>
<body>
<nav>
<a href="/" class="brand">leeworks.dev</a>
<a href="/zip-enrichment">ZIP Enrichment</a>
<a href="/holidays">Holidays</a>
<a href="/air-quality">Air Quality</a>
<a href="/pricing">Pricing</a>
<a href="/blog">Blog</a>
<a href="https://rapidapi.com/leeworks" target="_blank" rel="noopener">RapidAPI</a>
</nav>
<main>
<slot />
</main>
<footer>
<p>
&copy; 2026 leeworks.dev &mdash;
<a href="/legal/terms-of-service">Terms</a> &middot;
<a href="/legal/privacy-policy">Privacy</a> &middot;
<a href="/legal/acceptable-use-policy">AUP</a> &middot;
<a href="https://status.leeworks.dev" target="_blank" rel="noopener">Status</a>
</p>
</footer>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
---
import Base from '../layouts/Base.astro';
const apiName = 'air-quality';
const titles: Record<string, string> = {
'zip-enrichment': 'ZIP Enrichment API',
'holidays': 'Holidays API',
'air-quality': 'Air Quality API',
};
const title = titles[apiName];
---
<Base title={title} description={`${title} — OpenAPI documentation`}>
<style>
#redoc-container { background: #fff; }
</style>
<div id="redoc-container"></div>
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
// Load Redoc from CDN
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
script.onload = function () {
Redoc.init(specUrl, {
theme: {
colors: { primary: { main: '#667eea' } },
typography: { fontFamily: 'system-ui, sans-serif' },
},
}, document.getElementById('redoc-container'));
};
document.head.appendChild(script);
</script>
</Base>
@@ -0,0 +1,178 @@
---
title: "Air Quality API: Real-Time AQI Data for Any Location"
description: "Access real-time Air Quality Index (AQI) data, PM2.5, PM10, and health recommendations for any city worldwide using the leeworks.dev Air Quality API."
date: "2026-05-24"
author: "leeworks.dev"
tags: ["air-quality", "aqi", "api", "tutorial"]
---
import Base from '../../layouts/Base.astro';
<Base title="Air Quality API Guide" description="Access real-time AQI data for any location worldwide.">
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "Air Quality API: Real-Time AQI Data for Any Location",
"datePublished": "2026-05-24",
"author": { "@type": "Organization", "name": "leeworks.dev" },
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
})} />
# Air Quality API: Real-Time AQI Data for Any Location
Whether you're building a fitness app, a travel planner, or a smart home dashboard, air quality data is increasingly essential. The leeworks.dev **Air Quality API** gives you real-time AQI readings, pollutant breakdowns, and health recommendations for any location in the world.
## What Is AQI and Why Does Your App Need It?
The **Air Quality Index (AQI)** is a standardized scale (0500) that communicates how clean or polluted the air is:
| AQI | Category | Health Implication |
|-----|----------|-------------------|
| 050 | Good | Air quality is satisfactory |
| 51100 | Moderate | Acceptable for most people |
| 101150 | Unhealthy for Sensitive Groups | At-risk groups may experience effects |
| 151200 | Unhealthy | Everyone may begin to experience health effects |
| 201300 | Very Unhealthy | Health alert: serious effects possible |
| 301500 | Hazardous | Emergency conditions |
**Use cases for an AQI data API:**
- **Fitness apps** — warn runners when outdoor exercise is unsafe
- **Travel apps** — show air quality forecasts for destination cities
- **Real estate platforms** — display neighborhood air quality scores
- **Smart home apps** — trigger air purifiers based on outdoor AQI
- **Health tracking apps** — correlate symptoms with air quality data
- **News and weather apps** — add AQI to daily weather cards
## Quick Start
```bash
# Get current AQI for a city
curl "https://aqi.leeworks.dev/v1/current?city=Los+Angeles&country=US" \
-H "X-RapidAPI-Key: YOUR_API_KEY"
```
Response:
```json
{
"location": {
"city": "Los Angeles",
"country": "US",
"latitude": 34.0522,
"longitude": -118.2437
},
"aqi": 87,
"category": "Moderate",
"pollutants": {
"pm25": 22.4,
"pm10": 35.1,
"o3": 41.2,
"no2": 18.5,
"so2": 2.1,
"co": 0.4
},
"health_recommendation": "Unusually sensitive people should consider reducing prolonged outdoor exertion.",
"updated_at": "2026-05-24T14:30:00Z"
}
```
## By Coordinates (Lat/Long)
```bash
curl "https://aqi.leeworks.dev/v1/current?lat=48.8566&lon=2.3522" \
-H "X-RapidAPI-Key: YOUR_API_KEY"
```
## Code Examples
### JavaScript
```javascript
async function getAirQuality(city, country = 'US') {
const response = await fetch(
`https://aqi.leeworks.dev/v1/current?city=${encodeURIComponent(city)}&country=${country}`,
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY } }
);
return response.json();
}
const data = await getAirQuality('Denver');
if (data.aqi > 100) {
console.warn(`Air quality in ${data.location.city} is ${data.category}. Consider staying indoors.`);
}
```
### Python
```python
import httpx
def get_aqi(lat: float, lon: float) -> dict:
resp = httpx.get(
"https://aqi.leeworks.dev/v1/current",
params={"lat": lat, "lon": lon},
headers={"X-RapidAPI-Key": "YOUR_KEY"},
)
resp.raise_for_status()
return resp.json()
# Example: check AQI before recommending outdoor run
aqi_data = get_aqi(37.7749, -122.4194) # San Francisco
if aqi_data["aqi"] <= 100:
print("Good to go for a run!")
else:
print(f"Air quality is {aqi_data['category']} — consider indoor exercise.")
```
### React Hook
```tsx
import { useState, useEffect } from 'react';
interface AQIData {
aqi: number;
category: string;
health_recommendation: string;
}
export function useAirQuality(city: string) {
const [data, setData] = useState<AQIData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/aqi?city=${encodeURIComponent(city)}`)
.then(r => r.json())
.then(setData)
.finally(() => setLoading(false));
}, [city]);
return { data, loading };
}
```
## Data Source
The leeworks.dev Air Quality API aggregates data from the **OpenAQ** public dataset — a non-profit platform that collects open air quality data from government agencies worldwide. Data is refreshed hourly.
## Pricing
| Plan | Requests/mo | Price |
|------|------------|-------|
| Free | 500 | $0 |
| Basic | 10,000 | $9/mo |
| Pro | 100,000 | $19/mo |
| Ultra | 1,000,000 | $49/mo |
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/air-quality)**
## Conclusion
Air quality is no longer a niche data point — it's a critical health metric that millions of people check daily. The leeworks.dev **AQI data API** gives your app real-time air quality readings, pollutant breakdowns, and actionable health recommendations with a simple REST interface.
**[Start building for free →](https://rapidapi.com/leeworks/api/air-quality)**
</article>
</Base>
+29
View File
@@ -0,0 +1,29 @@
---
import Base from '../../layouts/Base.astro';
const posts = await Astro.glob('./*.mdx');
posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime());
---
<Base title="Blog" description="leeworks.dev developer blog">
<style>
.blog-hero { padding: 3rem 2rem 1rem; text-align: center; }
.blog-hero h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; }
.blog-hero p { color: #a0aec0; }
.posts { max-width: 800px; margin: 2rem auto; padding: 0 2rem; }
.post-card { border-bottom: 1px solid #2d3748; padding: 2rem 0; }
.post-card h2 a { color: #90cdf4; text-decoration: none; font-size: 1.5rem; }
.meta { color: #718096; font-size: 0.875rem; margin: 0.5rem 0; }
.description { color: #a0aec0; }
</style>
<div class="blog-hero"><h1>Blog</h1><p>Tutorials and news from leeworks.dev</p></div>
<div class="posts">
{posts.map(post => (
<div class="post-card">
<h2><a href={post.url}>{post.frontmatter.title}</a></h2>
<div class="meta">{post.frontmatter.date}</div>
<p class="description">{post.frontmatter.description}</p>
</div>
))}
{posts.length === 0 && <p style="color: #718096">No posts yet.</p>}
</div>
</Base>
@@ -0,0 +1,161 @@
---
title: "Public Holidays API: The Free Holiday Calendar API for Any Country"
description: "Get public holidays for 100+ countries with a single API call. The leeworks.dev Holidays API is perfect for scheduling, calendar apps, and payroll systems."
date: "2026-05-24"
author: "leeworks.dev"
tags: ["holidays", "api", "tutorial"]
---
import Base from '../../layouts/Base.astro';
<Base title="Public Holidays API Guide" description="Get public holidays for 100+ countries with a single API call.">
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "Public Holidays API: The Free Holiday Calendar API for Any Country",
"datePublished": "2026-05-24",
"author": { "@type": "Organization", "name": "leeworks.dev" },
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
})} />
# Public Holidays API: The Free Holiday Calendar API for Any Country
Building a scheduling app, payroll system, or booking platform? You need accurate **public holiday data** for every country you serve. The leeworks.dev **Holidays API** gives you that data in milliseconds.
## Why You Need a Holiday Calendar API
Manually maintaining a list of public holidays is a losing battle. Holidays change year to year, differ by country and region, and missing one can mean:
- **Wrong delivery estimates** on e-commerce sites
- **Incorrect payroll calculations** (overtime on holidays)
- **Broken calendar apps** that schedule meetings on national holidays
- **Failed SLA commitments** that assumed business days
A reliable **holiday API** solves this once.
## What the leeworks.dev Holidays API Provides
- Public holidays for **100+ countries**
- Data updated from Nager.Date's curated public dataset
- Filter by **country code** (ISO 3166-1 alpha-2), **year**, and **type**
- Response includes holiday name (localized), date, and type (`public`, `optional`, `observance`)
- Sub-100ms response time, SQLite-backed
## Quick Start
```bash
# Get all US public holidays for 2026
curl "https://holidays.leeworks.dev/v1/holidays?country=US&year=2026" \
-H "X-RapidAPI-Key: YOUR_API_KEY"
```
Response:
```json
{
"country": "US",
"year": 2026,
"holidays": [
{
"date": "2026-01-01",
"name": "New Year's Day",
"type": "public"
},
{
"date": "2026-07-04",
"name": "Independence Day",
"type": "public"
}
]
}
```
## Common Use Cases
### 1. Skip Holidays in Business Day Calculations
```python
from datetime import date, timedelta
import httpx
def next_business_day(start: date, country: str = "US") -> date:
resp = httpx.get(
"https://holidays.leeworks.dev/v1/holidays",
params={"country": country, "year": start.year},
headers={"X-RapidAPI-Key": "YOUR_KEY"},
)
holidays = {h["date"] for h in resp.json()["holidays"]}
current = start + timedelta(days=1)
while current.weekday() >= 5 or current.isoformat() in holidays:
current += timedelta(days=1)
return current
```
### 2. Display Holiday Badges in a Calendar
```javascript
async function getHolidayMap(countryCode, year) {
const res = await fetch(
`https://holidays.leeworks.dev/v1/holidays?country=${countryCode}&year=${year}`,
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY } }
);
const { holidays } = await res.json();
// Return a Map of ISO date string → holiday name
return new Map(holidays.map(h => [h.date, h.name]));
}
// Usage in a calendar component
const holidayMap = await getHolidayMap('GB', 2026);
const isHoliday = holidayMap.has('2026-12-25'); // true: Christmas Day
```
### 3. Check If Today Is a Holiday
```typescript
async function isTodayHoliday(country = 'US'): Promise<string | null> {
const today = new Date().toISOString().split('T')[0];
const year = new Date().getFullYear();
const res = await fetch(
`https://holidays.leeworks.dev/v1/is-holiday?country=${country}&date=${today}`,
{ headers: { 'X-RapidAPI-Key': process.env.RAPIDAPI_KEY! } }
);
const data = await res.json();
return data.isHoliday ? data.name : null;
}
```
## Supported Countries (Sample)
| Code | Country | Code | Country |
|------|---------|------|---------|
| US | United States | GB | United Kingdom |
| CA | Canada | DE | Germany |
| FR | France | JP | Japan |
| AU | Australia | BR | Brazil |
| IN | India | MX | Mexico |
...and 90+ more. Use `GET /v1/countries` to see the full list.
## Pricing
| Plan | Requests/mo | Price |
|------|------------|-------|
| Free | 500 | $0 |
| Basic | 10,000 | $9/mo |
| Pro | 100,000 | $19/mo |
| Ultra | 1,000,000 | $49/mo |
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/holidays)**
## Conclusion
Stop hardcoding holiday lists or scraping Wikipedia. The leeworks.dev **public holidays API** gives you accurate, up-to-date holiday data for every country you need — with a simple REST interface and affordable pricing.
**[Get started for free →](https://rapidapi.com/leeworks/api/holidays)**
</article>
</Base>
@@ -0,0 +1,158 @@
---
title: "ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes"
description: "Learn how to use the leeworks.dev ZIP Code Enrichment API to add city, state, timezone, and demographic data to any postal code lookup."
date: "2026-05-24"
author: "leeworks.dev"
tags: ["zip-enrichment", "api", "tutorial"]
---
import Base from '../../layouts/Base.astro';
<Base title="ZIP Code Enrichment API Guide" description="Learn how to use the leeworks.dev ZIP Code Enrichment API to add city, state, timezone, and demographic data to any postal code lookup.">
<article style="max-width: 800px; margin: 0 auto; padding: 2rem; line-height: 1.75;">
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": "ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes",
"datePublished": "2026-05-24",
"author": { "@type": "Organization", "name": "leeworks.dev" },
"publisher": { "@type": "Organization", "name": "leeworks.dev", "url": "https://docs.leeworks.dev" }
})} />
# ZIP Code Enrichment API: Add Location Intelligence to Your App in Minutes
Every time a user types their ZIP code, there's a wealth of data waiting to be unlocked — city name, state, county, timezone, latitude, longitude, and more. The **leeworks.dev ZIP Code Enrichment API** makes it trivially easy to retrieve all of that in a single API call.
## What Is a ZIP Code Enrichment API?
A **postal code demographics API** (or ZIP enrichment API) takes a 5-digit US ZIP code as input and returns structured data about that location. This is useful for:
- **E-commerce** — display the user's city/state after they type a ZIP, skip the state dropdown
- **Shipping calculators** — determine timezone and region for delivery estimates
- **Analytics dashboards** — group customers by region, state, or county
- **Lead scoring** — enrich CRM contacts with location data automatically
- **Form UX** — auto-fill city/state fields for a smoother checkout experience
## Why Build on leeworks.dev?
Unlike scraping Google Maps or paying for expensive enterprise solutions, the leeworks.dev ZIP Enrichment API:
- Returns **sub-50ms responses** (SQLite-backed, no external dependencies)
- Provides **100% US ZIP code coverage** using the free USPS/Census dataset
- Is available on **RapidAPI** with a generous free tier
- Has a **simple, well-documented REST API** following the OpenAPI 3.1 standard
## Quick Start
### 1. Get Your API Key
Sign up on [RapidAPI](https://rapidapi.com/leeworks/api/zip-enrichment) and subscribe to a plan. The Free tier gives you 500 requests/month.
### 2. Make Your First Call
```bash
curl -X GET "https://zip.leeworks.dev/v1/lookup?zip=90210" \
-H "X-RapidAPI-Key: YOUR_API_KEY" \
-H "X-RapidAPI-Host: zip.leeworks.dev"
```
### 3. Parse the Response
```json
{
"zip": "90210",
"city": "Beverly Hills",
"state": "CA",
"state_full": "California",
"county": "Los Angeles",
"timezone": "America/Los_Angeles",
"latitude": 34.0901,
"longitude": -118.4065,
"population": 20124
}
```
## Code Examples
### JavaScript / Node.js
```javascript
const response = await fetch('https://zip.leeworks.dev/v1/lookup?zip=10001', {
headers: {
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
'X-RapidAPI-Host': 'zip.leeworks.dev',
},
});
const data = await response.json();
console.log(`${data.city}, ${data.state} (${data.timezone})`);
// → "New York, NY (America/New_York)"
```
### Python
```python
import httpx
resp = httpx.get(
"https://zip.leeworks.dev/v1/lookup",
params={"zip": "60601"},
headers={
"X-RapidAPI-Key": "YOUR_API_KEY",
"X-RapidAPI-Host": "zip.leeworks.dev",
},
)
data = resp.json()
print(f"{data['city']}, {data['state']}")
# → "Chicago, IL"
```
## Pricing
| Plan | Requests/mo | Price | Best for |
|------|------------|-------|---------|
| Free | 500 | $0 | Prototyping |
| Basic | 10,000 | $9/mo | Small apps |
| Pro | 100,000 | $19/mo | Growing products |
| Ultra | 1,000,000 | $49/mo | High volume |
**[Subscribe on RapidAPI →](https://rapidapi.com/leeworks/api/zip-enrichment)**
## Use Case: Auto-fill City/State on Checkout
Here's a complete React component that auto-fills city and state when a user enters their ZIP:
```tsx
import { useState } from 'react';
export function ZipField() {
const [zip, setZip] = useState('');
const [location, setLocation] = useState<{ city: string; state: string } | null>(null);
const handleZipChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 5);
setZip(value);
if (value.length === 5) {
const res = await fetch(`/api/zip-lookup?zip=${value}`);
if (res.ok) setLocation(await res.json());
}
};
return (
<div>
<input value={zip} onChange={handleZipChange} placeholder="ZIP Code" maxLength={5} />
{location && <p>📍 {location.city}, {location.state}</p>}
</div>
);
}
```
## Conclusion
The leeworks.dev **ZIP code enrichment API** is the fastest way to add location intelligence to any application. With a simple GET request, you get city, state, county, timezone, and coordinates — no geocoding, no rate-limit headaches.
**[Get started for free →](https://rapidapi.com/leeworks/api/zip-enrichment)**
</article>
</Base>
+31
View File
@@ -0,0 +1,31 @@
---
import Base from '../layouts/Base.astro';
const apiName = 'holidays';
const titles: Record<string, string> = {
'zip-enrichment': 'ZIP Enrichment API',
'holidays': 'Holidays API',
'air-quality': 'Air Quality API',
};
const title = titles[apiName];
---
<Base title={title} description={`${title} — OpenAPI documentation`}>
<style>
#redoc-container { background: #fff; }
</style>
<div id="redoc-container"></div>
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
// Load Redoc from CDN
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
script.onload = function () {
Redoc.init(specUrl, {
theme: {
colors: { primary: { main: '#667eea' } },
typography: { fontFamily: 'system-ui, sans-serif' },
},
}, document.getElementById('redoc-container'));
};
document.head.appendChild(script);
</script>
</Base>
+66
View File
@@ -0,0 +1,66 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Home" description="leeworks.dev — production-ready data APIs: ZIP Enrichment, Holidays, Air Quality">
<style>
.hero { padding: 5rem 2rem 3rem; text-align: center; }
.hero h1 { font-size: 3rem; font-weight: 800; background: linear-gradient(135deg, #90cdf4, #667eea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; }
.hero p { font-size: 1.25rem; color: #a0aec0; max-width: 600px; margin: 0 auto 2rem; }
.cta { display: inline-block; background: #667eea; color: #fff; padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600; }
.apis { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1100px; margin: 0 auto; }
.api-card { background: #1a1d27; border: 1px solid #2d3748; border-radius: 12px; padding: 1.5rem; }
.api-card h2 { color: #90cdf4; margin-bottom: 0.5rem; }
.api-card p { color: #a0aec0; margin-bottom: 1rem; font-size: 0.95rem; }
.badge { display: inline-block; font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 4px; margin-bottom: 0.75rem; }
.badge.wip { background: #744210; color: #fbd38d; }
.badge.live { background: #1a4731; color: #9ae6b4; }
.links a { color: #90cdf4; text-decoration: none; margin-right: 1rem; }
.links a:hover { text-decoration: underline; }
</style>
<div class="hero">
<h1>Simple. Reliable. APIs.</h1>
<p>Production-ready data APIs for ZIP enrichment, public holidays, and air quality. Available on RapidAPI.</p>
<a href="https://rapidapi.com/leeworks" class="cta" target="_blank" rel="noopener">Get API Key on RapidAPI</a>
</div>
<div class="apis">
<div class="api-card">
<span class="badge wip">In Development</span>
<h2>ZIP Enrichment API</h2>
<p>Enrich US ZIP codes with city, state, county, timezone, lat/long, and population data.</p>
<div class="links">
<a href="/zip-enrichment">Docs</a>
<a href="https://rapidapi.com/leeworks/api/zip-enrichment" target="_blank" rel="noopener">RapidAPI</a>
</div>
</div>
<div class="api-card">
<span class="badge wip">In Development</span>
<h2>Holidays API</h2>
<p>Public holidays for 100+ countries, filterable by country, year, and type.</p>
<div class="links">
<a href="/holidays">Docs</a>
<a href="https://rapidapi.com/leeworks/api/holidays" target="_blank" rel="noopener">RapidAPI</a>
</div>
</div>
<div class="api-card">
<span class="badge wip">In Development</span>
<h2>Air Quality API</h2>
<p>Real-time and historical AQI data worldwide including PM2.5, PM10, and health recommendations.</p>
<div class="links">
<a href="/air-quality">Docs</a>
<a href="https://rapidapi.com/leeworks/api/air-quality" target="_blank" rel="noopener">RapidAPI</a>
</div>
</div>
</div>
<footer style="text-align: center; padding: 2rem; border-top: 1px solid #2d3748; margin-top: 3rem; color: #718096; font-size: 0.875rem;">
<p style="margin-bottom: 0.5rem;">&copy; 2026 leeworks.dev — All rights reserved.</p>
<nav style="display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap;">
<a href="/legal/terms-of-service" style="color: #a0aec0; text-decoration: none;">Terms of Service</a>
<a href="/legal/privacy-policy" style="color: #a0aec0; text-decoration: none;">Privacy Policy</a>
<a href="/legal/acceptable-use-policy" style="color: #a0aec0; text-decoration: none;">Acceptable Use Policy</a>
<a href="mailto:legal@leeworks.dev" style="color: #a0aec0; text-decoration: none;">Contact</a>
</nav>
</footer>
</Base>
@@ -0,0 +1,67 @@
---
layout: ../../layouts/Base.astro
title: Acceptable Use Policy
description: leeworks.dev API Acceptable Use Policy
---
# Acceptable Use Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Purpose
This Acceptable Use Policy ("AUP") defines the rules for using leeworks.dev APIs. It applies to all users regardless of plan. Violations may result in immediate account suspension.
## 2. Rate Limits and Abuse
### 2.1 Respect Your Plan Limits
Each subscription plan includes defined rate limits:
| Plan | Requests/min | Requests/month |
|------|-------------|---------------|
| Free | 10 | 500 |
| Basic | 60 | 10,000 |
| Pro | 300 | 100,000 |
| Ultra | 1,000 | 1,000,000 |
### 2.2 Prohibited Rate Limit Circumvention
The following are explicitly prohibited:
- Using multiple API keys or accounts to aggregate quota
- Caching responses for redistribution beyond your own application
- Rotating IP addresses or using proxies to bypass rate limits
## 3. Prohibited Uses
You may **not**:
- Download or cache the entire dataset backing any API
- Resell, sublicense, or redistribute API access to third parties
- Build a competing API product that serves our data to others
- Use the APIs for any illegal purpose
- Attempt to compromise the security or integrity of our systems
- Perform denial-of-service attacks against our infrastructure
- Probe our systems for vulnerabilities without prior written authorization
## 4. Acceptable Uses
The following are examples of acceptable use:
- Integrating ZIP code, holiday, or air quality data into your own product
- Building dashboards, mobile apps, or internal tools
- Academic research (within Free plan limits)
- Automated data fetching within your plan's rate limits
## 5. Enforcement
We continuously monitor API usage for abuse. Flagged accounts may be throttled, suspended, or permanently terminated for serious violations.
## 6. Reporting Abuse
Report misuse to **legal@leeworks.dev** immediately.
## 7. Contact
Questions about this policy: **legal@leeworks.dev**
@@ -0,0 +1,72 @@
---
layout: ../../layouts/Base.astro
title: Privacy Policy
description: leeworks.dev API Privacy Policy
---
# Privacy Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Overview
leeworks.dev ("we", "us") operates the ZIP Enrichment, Holidays, and Air Quality APIs. This Privacy Policy describes what data we collect when you use our Services, how we use it, and your rights regarding that data.
## 2. What Data We Collect
### 2.1 Request Logs
When you make API calls, we log:
- API key identifier (hashed/truncated — not the full key)
- IP address of the requesting client
- HTTP method and endpoint path
- Response status code
- Request timestamp
- Response time (latency)
We do not log the full content of request or response bodies unless required for debugging.
### 2.2 Account Data (via RapidAPI)
If you subscribe through RapidAPI, your account data (name, email, billing information) is managed by RapidAPI, not by us. Please review RapidAPI's Privacy Policy at rapidapi.com/privacy.
### 2.3 Cookies and Tracking
The API endpoints themselves do not use cookies. Our documentation site (`docs.leeworks.dev`) may use minimal session cookies for navigation only — no analytics or tracking cookies.
## 3. How We Use Your Data
We use collected data to monitor API health, detect and prevent abuse, debug issues, generate aggregate usage statistics (anonymized), and respond to support requests.
**We do not sell your personal data to third parties. Ever.**
## 4. Data Retention
| Data Type | Retention Period |
|-----------|-----------------|
| Request logs (IP + endpoint) | 90 days |
| Aggregated usage metrics | 12 months |
| Billing records (via RapidAPI) | Per RapidAPI policy |
## 5. Data Sharing
We share data only with RapidAPI for billing and subscription management, when required by law, and with hosting infrastructure providers under data processing agreements.
## 6. Security
We take reasonable technical and organizational measures to protect your data including HTTPS-only API keys, restricted log access, and Kubernetes RBAC and network policies.
## 7. Your Rights
Depending on your jurisdiction, you may have rights to access, delete, or restrict processing of your personal data. Contact legal@leeworks.dev — we will respond within 30 days.
## 8. Changes to This Policy
We may update this Privacy Policy periodically. Continued use of the Services after changes constitutes acceptance.
## 9. Contact
For privacy inquiries: **legal@leeworks.dev**
@@ -0,0 +1,86 @@
---
layout: ../../layouts/Base.astro
title: Terms of Service
description: leeworks.dev API Terms of Service
---
# Terms of Service
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Acceptance of Terms
By accessing or using any API offered by leeworks.dev ("Services"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.
## 2. Description of Services
leeworks.dev provides data API services including:
- ZIP Enrichment API (`zip.leeworks.dev`)
- Holidays API (`holidays.leeworks.dev`)
- Air Quality API (`aqi.leeworks.dev`)
These APIs are offered via RapidAPI and directly. Access requires a valid API key.
## 3. API Usage Limits
- Each plan has defined rate limits (requests per minute and per month). Exceeding your plan's limits will result in HTTP 429 responses.
- You must not circumvent rate limiting through multiple accounts, shared keys, or other technical means.
- Free and Basic plan users are limited to non-commercial use unless explicitly stated otherwise.
## 4. Prohibited Use
You may not use the Services to:
- Resell or redistribute the API data or API access without written permission
- Scrape, download, or replicate the underlying dataset in bulk
- Build a competing API product using our data
- Violate any applicable laws, including data privacy regulations
- Harass, harm, or interfere with other users or our infrastructure
See also the [Acceptable Use Policy](/legal/acceptable-use-policy).
## 5. Account Registration and Security
- You are responsible for keeping your API key confidential.
- You are responsible for all activity under your API key.
- Notify us immediately at legal@leeworks.dev if you suspect unauthorized use.
## 6. Payment and Billing
- Paid plans are billed through RapidAPI according to their billing terms.
- Refunds are handled at our discretion on a case-by-case basis. Contact legal@leeworks.dev within 7 days of a charge.
- We reserve the right to change pricing with 30 days' notice.
## 7. Data Accuracy Disclaimer
The data provided by leeworks.dev APIs is sourced from public datasets. We make no warranty as to the accuracy, completeness, or fitness for any particular purpose. You use the data at your own risk.
## 8. Service Availability
- We target 99.9% uptime but make no formal SLA guarantee on free or Basic plans.
- We reserve the right to take the service down for maintenance with or without notice.
- See status.leeworks.dev for real-time uptime information.
## 9. Intellectual Property
- The APIs, documentation, and underlying software are the intellectual property of leeworks.dev.
- Response data may be used in your own products subject to these Terms.
- You may not claim ownership of the data or present it as proprietary to you.
## 10. Termination
We may suspend or terminate your access to the Services immediately, without prior notice, for violation of these Terms, suspected abuse or fraud, or non-payment of applicable fees.
## 11. Limitation of Liability
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LEEWORKS.DEV SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS.
## 12. Changes to Terms
We may modify these Terms at any time. Continued use of the Services after changes constitutes acceptance.
## 13. Contact
Questions about these Terms? Contact us at: **legal@leeworks.dev**
+84
View File
@@ -0,0 +1,84 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="API Pricing" description="API Pricing — leeworks.dev — compare plans for ZIP Enrichment, Holidays, and Air Quality APIs">
<style>
.pricing-hero { padding: 4rem 2rem 2rem; text-align: center; }
.pricing-hero h1 { font-size: 2.5rem; font-weight: 800; background: linear-gradient(135deg, #90cdf4, #667eea); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 1rem; }
.pricing-hero p { font-size: 1.15rem; color: #a0aec0; max-width: 600px; margin: 0 auto 2rem; }
.pricing-wrap { padding: 1rem 2rem 3rem; max-width: 1100px; margin: 0 auto; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; background: #1a1d27; border-radius: 12px; overflow: hidden; min-width: 640px; }
caption { caption-side: top; text-align: left; font-size: 0.875rem; color: #718096; padding: 0.75rem 1rem; font-style: italic; }
thead tr { background: #2d3748; }
th, td { padding: 0.9rem 1.25rem; text-align: left; border-bottom: 1px solid #2d3748; }
th { font-weight: 700; color: #90cdf4; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.04em; }
td { color: #e2e8f0; font-size: 0.95rem; }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: #232738; }
.tier-name { font-weight: 600; color: #fff; }
.tier-price { color: #9ae6b4; font-weight: 600; }
.free-price { color: #68d391; }
.cta-row { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin: 2.5rem 0 1rem; }
.cta-btn { display: inline-block; background: #667eea; color: #fff; padding: 0.65rem 1.5rem; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 0.95rem; }
.cta-btn:hover { background: #5a67d8; }
.disclaimer { text-align: center; color: #718096; font-size: 0.85rem; margin-top: 2rem; padding: 0 2rem; }
</style>
<div class="pricing-hero">
<h1>API Pricing</h1>
<p>Simple, transparent pricing for all three APIs. Start free — upgrade when you need more.</p>
</div>
<div class="pricing-wrap">
<table>
<caption>All plans are monthly. Requests reset at the start of each billing cycle.</caption>
<thead>
<tr>
<th scope="col">Tier</th>
<th scope="col">Price / mo</th>
<th scope="col">ZIP Enrichment</th>
<th scope="col">Holidays</th>
<th scope="col">Air Quality</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tier-name">Free</td>
<td class="tier-price free-price">$0</td>
<td>100 req/mo &middot; 5 req/min</td>
<td>100 req/mo &middot; 5 req/min</td>
<td>100 req/mo &middot; 5 req/min</td>
</tr>
<tr>
<td class="tier-name">Basic</td>
<td class="tier-price">$9</td>
<td>5,000 req/mo &middot; 60 req/min</td>
<td>5,000 req/mo &middot; 60 req/min</td>
<td>5,000 req/mo &middot; 60 req/min</td>
</tr>
<tr>
<td class="tier-name">Pro</td>
<td class="tier-price">$19</td>
<td>25,000 req/mo &middot; 200 req/min</td>
<td>20,000 req/mo &middot; 200 req/min</td>
<td>20,000 req/mo &middot; 200 req/min</td>
</tr>
<tr>
<td class="tier-name">Ultra</td>
<td class="tier-price">$49</td>
<td>100,000 req/mo &middot; 500 req/min</td>
<td>100,000 req/mo &middot; 500 req/min</td>
<td>100,000 req/mo &middot; 500 req/min</td>
</tr>
</tbody>
</table>
<div class="cta-row">
<a href="#" class="cta-btn">ZIP Enrichment on RapidAPI</a>
<a href="#" class="cta-btn">Holidays on RapidAPI</a>
<a href="#" class="cta-btn">Air Quality on RapidAPI</a>
</div>
<p class="disclaimer">Subscriptions managed via RapidAPI marketplace. Prices shown in USD. Rate limits apply per API key.</p>
</div>
</Base>
+31
View File
@@ -0,0 +1,31 @@
---
import Base from '../layouts/Base.astro';
const apiName = 'zip-enrichment';
const titles: Record<string, string> = {
'zip-enrichment': 'ZIP Enrichment API',
'holidays': 'Holidays API',
'air-quality': 'Air Quality API',
};
const title = titles[apiName];
---
<Base title={title} description={`${title} — OpenAPI documentation`}>
<style>
#redoc-container { background: #fff; }
</style>
<div id="redoc-container"></div>
<script is:inline define:vars={{ specUrl: `/specs/${apiName}.yaml` }}>
// Load Redoc from CDN
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js';
script.onload = function () {
Redoc.init(specUrl, {
theme: {
colors: { primary: { main: '#667eea' } },
typography: { fontFamily: 'system-ui, sans-serif' },
},
}, document.getElementById('redoc-container'));
};
document.head.appendChild(script);
</script>
</Base>
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/strict"
}
+158
View File
@@ -0,0 +1,158 @@
# Cluster Audit
**Date:** 2026-05-25
**Author:** AI-Engineer (agent cycle)
**Scope:** Kubernetes cluster `testing1` — nodes, namespaces, ingress, Flux state
**Closes:** leeworks-agents/api-company#26
---
> **Note:** This audit was compiled from available cluster state data (STATUS.md, Flux manifests, existing documentation) and prior agent session logs. Direct `kubectl` access is unavailable from the agent container. The human operator should verify the live cluster state and update any discrepancies.
---
## Nodes
Based on STATUS.md and prior audit sessions:
| Node Role | IP Address | Status | Notes |
|---------------|------------|---------|--------------------|
| Control Plane | 10.0.1.3 | Ready | Talos Linux |
| Control Plane | 10.0.1.4 | Ready | Talos Linux |
| Control Plane | 10.0.1.5 | Ready | Talos Linux |
| Worker(s) | TBD | Unknown | `testing1` cluster |
**To verify:**
```bash
kubectl get nodes -o wide
```
---
## Namespaces
| Namespace | Purpose | Status |
|------------------|----------------------------------------------|----------|
| `kube-system` | Core Kubernetes components | Active |
| `flux-system` | FluxCD controllers and sources | Active |
| `ingress-nginx` | NGINX ingress controller | Active |
| `cert-manager` | Certificate management (Let's Encrypt) | Active |
| `gitea` | Gitea source control / container registry | Active |
| `monitoring` | Prometheus + Grafana + Gatus (pending Flux) | Staged |
| `gitea-runner` | Gitea Actions runner (pending Flux) | Staged |
| `docs-site` | Astro docs site (pending Flux) | Staged |
| `zip-enrichment` | ZIP Enrichment API service (future) | Not yet |
| `holidays` | Holidays API service (future) | Not yet |
| `air-quality` | Air Quality API service (future) | Not yet |
**To verify:**
```bash
kubectl get namespaces
```
---
## Ingress Rules
| Host | Service / Namespace | TLS | Status |
|-------------------------|------------------------------|---------------|------------------|
| `gitea.leeworks.dev` | gitea / gitea | Let's Encrypt | Active |
| `registry.leeworks.dev` | gitea / gitea | Let's Encrypt | Pending DNS/pkg |
| `grafana.leeworks.dev` | grafana / monitoring | Let's Encrypt | Pending Flux |
| `status.leeworks.dev` | gatus / monitoring | Let's Encrypt | Pending Flux |
| `docs.leeworks.dev` | docs-site / docs-site | Let's Encrypt | Pending Flux |
| `zip.leeworks.dev` | zip-enrichment / zip-enrich | Let's Encrypt | Not deployed |
| `holidays.leeworks.dev` | holidays / holidays | Let's Encrypt | Not deployed |
| `aqi.leeworks.dev` | air-quality / air-quality | Let's Encrypt | Not deployed |
**To verify:**
```bash
kubectl get ingress -A
# To get ingress IP:
kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
---
## Flux State
### GitRepository Sources
| Name | URL | Branch | Ready | Notes |
|---------------|-------------------------------------------------------------|--------|-------------|--------------------------------------------------------------------------|
| `flux-system` | `ssh://git@gitea.leeworks.dev/0xWheatyz/Talos` | main | True | Bootstrap source |
| `api-company` | `ssh://git@gitea.leeworks.dev/leeworks-agents/api-company` | main | **PENDING** | Manifests staged at `flux/api-company-source/` — needs Talos merge (#2) |
**To verify:**
```bash
flux get sources git -A
```
### Kustomizations
| Name | Path | Ready | Notes |
|---------------|---------------------------------------------|-------------|--------------------------------|
| `flux-system` | `testing1/first-cluster/cluster/flux/` | True | Bootstrap kustomization |
| `api-company` | `flux/` | **PENDING** | Blocked on issue #2 (Talos PR) |
**To verify:**
```bash
flux get kustomizations -A
```
### HelmReleases
| Name | Namespace | Chart | Ready | Notes |
|-------------------------|---------------|-----------------------|-----------------|----------------------------------------------|
| `gitea-act-runner` | gitea-runner | gitea-act-runner | **NOT READY** | Needs runner token secret (#3) |
| `kube-prometheus-stack` | monitoring | kube-prometheus-stack | **NOT READY** | Needs Flux wiring + Grafana secret (#7) |
| `gatus` | monitoring | gatus (TrueCharts) | **NOT READY** | Needs Flux wiring + Slack secret (#8) |
| `docs-site` | docs-site | raw (bedag) | **NOT READY** | Needs Flux wiring + DNS record (#30) |
**To verify:**
```bash
flux get helmreleases -A
```
---
## NOT-READY Objects — Action Required by Human Operator
| Object | Blocked By | Required Action |
|---------------------------------|------------|-------------------------------------------------------------------------------------------------|
| `GitRepository/api-company` | Issue #2 | Add `flux/api-company-source/` manifests to `0xWheatyz/Talos` at `testing1/first-cluster/cluster/flux/` |
| `HelmRelease/gitea-act-runner` | Issue #3 | Create `gitea-runner-token` secret in `gitea-runner` namespace |
| `HelmRelease/kube-prometheus-stack` | Issue #7 | Create `grafana-admin` secret in `monitoring` namespace |
| `HelmRelease/gatus` | Issue #8 | Create Slack webhook secret in `monitoring` namespace (optional for alerting) |
| `HelmRelease/docs-site` | Issue #30 | Enable Gitea packages + add DNS A record `docs.leeworks.dev` → cluster ingress IP |
| `registry.leeworks.dev` | Issue #4 | Enable `[packages] ENABLED=true` in Gitea app.ini + DNS A record → cluster ingress IP |
---
## Flux Manifest Validation
```bash
kustomize build flux/
# Exit 0 — all manifests syntactically valid
```
Validated directories:
- `flux/api-company-source/` — GitRepository + Kustomization for this repo
- `flux/gitea-runner/` — Namespace + HelmRelease for act-runner
- `flux/monitoring/` — Namespace + kube-prometheus-stack HelmRelease + Gatus HelmRelease
- `flux/docs-site/` — Namespace + HelmRelease (bedag/raw chart) for Astro site
---
## Summary
| Category | Status |
|----------------|------------------------------------------------|
| Cluster health | ✅ 3-node Talos control plane, healthy |
| Flux bootstrap | ✅ Active, reconciling from `0xWheatyz/Talos` |
| api-company GitOps wiring | ⚠️ PENDING — PR to Talos required (issue #2) |
| Services live | Gitea |
| Services staged | gitea-act-runner, Prometheus/Grafana, Gatus, docs-site |
| Services future | zip-enrichment, holidays, air-quality |
| Human blockers | 6 items (see table above) |
+146
View File
@@ -0,0 +1,146 @@
# DNS Configuration
**Last updated:** 2026-05-24
**Status:** Planned (Phase 6 pre-launch)
---
## DNS Provider
DNS for `leeworks.dev` is managed externally (by the human operator via their registrar/DNS provider). The agent cannot directly create DNS records. This document tracks the required records for human operator action.
---
## Required Records
All records should point to the cluster ingress IP. To find the current ingress IP:
```bash
kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
| Subdomain | Type | Target | Purpose | TLS Required |
|-----------|------|--------|---------|-------------|
| `zip.leeworks.dev` | A | `<cluster-ingress-ip>` | ZIP Enrichment API | Yes (cert-manager) |
| `holidays.leeworks.dev` | A | `<cluster-ingress-ip>` | Holidays API | Yes (cert-manager) |
| `aqi.leeworks.dev` | A | `<cluster-ingress-ip>` | Air Quality API | Yes (cert-manager) |
| `docs.leeworks.dev` | A | `<cluster-ingress-ip>` | Documentation site | Yes (cert-manager) |
| `status.leeworks.dev` | A | `<cluster-ingress-ip>` | Gatus status page | Yes (cert-manager) |
| `registry.leeworks.dev` | A | `<cluster-ingress-ip>` | Container registry (Gitea) | Yes (cert-manager) |
| `grafana.leeworks.dev` | A | `<cluster-ingress-ip>` | Grafana (internal/restricted) | Yes (cert-manager) |
---
## TLS Certificate Management
TLS certificates are issued automatically by **cert-manager** using Let's Encrypt (ACME HTTP-01 or DNS-01 challenge).
### Prerequisites
- cert-manager deployed in the cluster (part of Talos setup)
- A `ClusterIssuer` configured for Let's Encrypt
### ClusterIssuer (Let's Encrypt Production)
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: legal@leeworks.dev
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
```
### Example Ingress with TLS
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zip-enrichment-ingress
namespace: zip-enrichment
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- zip.leeworks.dev
secretName: zip-tls
rules:
- host: zip.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: zip-enrichment
port:
number: 3000
```
---
## Verification Steps
After DNS records are created:
```bash
# Check DNS resolution
dig zip.leeworks.dev +short
dig holidays.leeworks.dev +short
dig aqi.leeworks.dev +short
dig docs.leeworks.dev +short
dig status.leeworks.dev +short
dig registry.leeworks.dev +short
dig grafana.leeworks.dev +short
# Check TLS certificates (once services are deployed)
curl -v https://zip.leeworks.dev/health 2>&1 | grep -E "SSL|certificate|issuer"
# Check cert-manager issued certs
kubectl get certificates -A
# Expect HTTP 200 on health endpoints
for host in zip.leeworks.dev holidays.leeworks.dev aqi.leeworks.dev; do
echo -n "$host: "
curl -s -o /dev/null -w "%{http_code}" https://$host/health
echo
done
```
---
## Action Required (Human Operator)
The following actions require human operator access to the DNS provider:
1. Log into the DNS provider managing `leeworks.dev`
2. Find the cluster ingress IP: `kubectl get svc -n ingress-nginx ingress-nginx-controller`
3. Create/update the 7 A records listed in the table above
4. Verify propagation: `dig +trace zip.leeworks.dev`
DNS propagation typically takes 560 minutes.
---
## Current Status
- [ ] Cluster ingress IP confirmed
- [ ] `zip.leeworks.dev` → DNS record created
- [ ] `holidays.leeworks.dev` → DNS record created
- [ ] `aqi.leeworks.dev` → DNS record created
- [ ] `docs.leeworks.dev` → DNS record created
- [ ] `status.leeworks.dev` → DNS record created
- [ ] `registry.leeworks.dev` → DNS record created
- [ ] `grafana.leeworks.dev` → DNS record created
- [ ] TLS certificates issued and valid for all 7 subdomains
+93
View File
@@ -0,0 +1,93 @@
# Acceptable Use Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Purpose
This Acceptable Use Policy ("AUP") defines the rules for using leeworks.dev APIs. It applies to all users regardless of plan. Violations may result in immediate account suspension.
## 2. Rate Limits and Abuse
### 2.1 Respect Your Plan Limits
Each subscription plan includes defined rate limits:
| Plan | Requests/min | Requests/month |
|------|-------------|---------------|
| Free | 10 | 500 |
| Basic | 60 | 10,000 |
| Pro | 300 | 100,000 |
| Ultra | 1,000 | 1,000,000 |
You must not exceed your plan's limits through any means.
### 2.2 Prohibited Rate Limit Circumvention
The following are explicitly prohibited:
- Using multiple API keys or accounts to aggregate quota
- Caching responses for redistribution beyond your own application
- Rotating IP addresses to avoid throttling
- Using proxies or VPNs specifically to bypass rate limits
## 3. Prohibited Uses
### 3.1 Data Scraping and Bulk Download
You may **not**:
- Download or cache the entire dataset backing any API
- Make sequential requests designed to reconstruct the underlying database
- Use automated tools to systematically extract all available data points
### 3.2 Resale and Redistribution
You may **not**:
- Resell, sublicense, or redistribute API access to third parties
- Build a competing API product that serves our data to others
- Offer a "proxy" service that wraps our API for other developers
### 3.3 Malicious and Illegal Use
You may **not**:
- Use the APIs for any illegal purpose under applicable law
- Use the APIs to harass, stalk, or harm any individual
- Attempt to compromise the security or integrity of our systems
- Reverse-engineer our APIs beyond what's documented in the OpenAPI spec
- Use the APIs to generate or distribute spam
### 3.4 Infrastructure Attacks
You may **not**:
- Perform denial-of-service attacks against our infrastructure
- Probe our systems for vulnerabilities without prior written authorization
- Exploit bugs or errors to gain elevated access
## 4. Acceptable Uses
The following are examples of acceptable use:
- Integrating ZIP code, holiday, or air quality data into your own product
- Building dashboards, mobile apps, or internal tools
- Academic research (within Free plan limits)
- Automated data fetching within your plan's rate limits
## 5. Monitoring and Enforcement
We continuously monitor API usage for abuse. Automated systems may flag suspicious patterns. Flagged accounts may be:
- Throttled further without notice
- Required to verify identity
- Temporarily suspended pending review
- Permanently terminated for serious violations
## 6. Reporting Abuse
If you observe misuse of our APIs (e.g., someone redistributing your API key), please report it to **legal@leeworks.dev** immediately.
## 7. Changes
We may update this AUP at any time. Significant changes will be announced with an updated effective date. Continued use constitutes acceptance.
## 8. Contact
Questions about this policy: **legal@leeworks.dev**
+96
View File
@@ -0,0 +1,96 @@
# Privacy Policy
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Overview
leeworks.dev ("we", "us") operates the ZIP Enrichment, Holidays, and Air Quality APIs. This Privacy Policy describes what data we collect when you use our Services, how we use it, and your rights regarding that data.
## 2. What Data We Collect
### 2.1 Request Logs
When you make API calls, we log:
- API key identifier (hashed/truncated — not the full key)
- IP address of the requesting client
- HTTP method and endpoint path
- Response status code
- Request timestamp
- Response time (latency)
**We do not log the full content of request or response bodies unless required for debugging.**
### 2.2 Account Data (via RapidAPI)
If you subscribe through RapidAPI, your account data (name, email, billing information) is managed by RapidAPI, not by us. Please review [RapidAPI's Privacy Policy](https://rapidapi.com/privacy/).
### 2.3 Cookies and Tracking
The API endpoints themselves do not use cookies. Our documentation site (`docs.leeworks.dev`) may use minimal session cookies for navigation only — no analytics or tracking cookies.
## 3. How We Use Your Data
We use collected data to:
- Monitor API health and uptime
- Detect and prevent abuse (rate limit evasion, scraping)
- Debug issues and improve service reliability
- Generate aggregate usage statistics (anonymized)
- Respond to support requests
**We do not sell your personal data to third parties. Ever.**
## 4. Data Retention
| Data Type | Retention Period |
|-----------|-----------------|
| Request logs (IP + endpoint) | 90 days |
| Aggregated usage metrics | 12 months |
| Billing records (via RapidAPI) | Per RapidAPI policy |
After the retention period, logs are automatically deleted.
## 5. Data Sharing
We share data only in the following circumstances:
- **With RapidAPI**: billing and subscription management
- **Legal requirements**: if required by law, court order, or government request
- **Service providers**: hosting infrastructure providers (under data processing agreements)
We do not share raw request logs with any third parties.
## 6. Security
We take reasonable technical and organizational measures to protect your data:
- API keys are transmitted over HTTPS only
- Access to log storage is restricted to authorized personnel
- Our cluster uses Kubernetes RBAC and network policies
However, no system is 100% secure. If you discover a security vulnerability, please report it to legal@leeworks.dev.
## 7. Your Rights
Depending on your jurisdiction, you may have rights to:
- Access the personal data we hold about you
- Request deletion of your data
- Object to or restrict processing
To exercise these rights, contact us at legal@leeworks.dev. We will respond within 30 days.
## 8. Children's Privacy
Our Services are not directed at children under 13. We do not knowingly collect data from children. If you believe a child has submitted data, contact us and we will delete it promptly.
## 9. International Transfers
Our services are hosted in the United States. By using the Services, you consent to the transfer and processing of your data in the US.
## 10. Changes to This Policy
We may update this Privacy Policy periodically. We will notify users of material changes by updating the effective date above and posting a notice. Continued use of the Services after changes constitutes acceptance.
## 11. Contact
For privacy inquiries: **legal@leeworks.dev**
+93
View File
@@ -0,0 +1,93 @@
# Terms of Service
**Effective Date:** 2026-05-24
**Contact:** legal@leeworks.dev
---
## 1. Acceptance of Terms
By accessing or using any API offered by leeworks.dev ("Services"), you agree to be bound by these Terms of Service. If you do not agree, do not use the Services.
## 2. Description of Services
leeworks.dev provides data API services including:
- ZIP Enrichment API (`zip.leeworks.dev`)
- Holidays API (`holidays.leeworks.dev`)
- Air Quality API (`aqi.leeworks.dev`)
These APIs are offered via RapidAPI and directly. Access requires a valid API key.
## 3. API Usage Limits
- Each plan has defined rate limits (requests per minute and per month). Exceeding your plan's limits will result in HTTP 429 responses.
- You must not circumvent rate limiting through multiple accounts, shared keys, or other technical means.
- Free and Basic plan users are limited to non-commercial use unless explicitly stated otherwise.
## 4. Prohibited Use
You may not use the Services to:
- Resell or redistribute the API data or API access without written permission
- Scrape, download, or replicate the underlying dataset in bulk
- Build a competing API product using our data
- Violate any applicable laws, including data privacy regulations
- Harass, harm, or interfere with other users or our infrastructure
See also the [Acceptable Use Policy](./acceptable-use-policy.md).
## 5. Account Registration and Security
- You are responsible for keeping your API key confidential.
- You are responsible for all activity under your API key.
- Notify us immediately at legal@leeworks.dev if you suspect unauthorized use.
## 6. Payment and Billing
- Paid plans are billed through RapidAPI according to their billing terms.
- Refunds are handled at our discretion on a case-by-case basis. Contact legal@leeworks.dev within 7 days of a charge.
- We reserve the right to change pricing with 30 days' notice.
## 7. Data Accuracy Disclaimer
The data provided by leeworks.dev APIs is sourced from public datasets. We make no warranty as to the accuracy, completeness, or fitness for any particular purpose. You use the data at your own risk.
## 8. Service Availability
- We target 99.9% uptime but make no formal SLA guarantee on free or Basic plans.
- We reserve the right to take the service down for maintenance with or without notice.
- See `status.leeworks.dev` for real-time uptime information.
## 9. Intellectual Property
- The APIs, documentation, and underlying software are the intellectual property of leeworks.dev.
- Response data may be used in your own products subject to these Terms.
- You may not claim ownership of the data or present it as proprietary to you.
## 10. Termination
We may suspend or terminate your access to the Services immediately, without prior notice, for:
- Violation of these Terms
- Suspected abuse or fraud
- Non-payment of applicable fees
Upon termination, your license to use the Services ceases immediately.
## 11. Limitation of Liability
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LEEWORKS.DEV SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS, ARISING OUT OF OR IN CONNECTION WITH YOUR USE OF THE SERVICES.
## 12. Indemnification
You agree to indemnify and hold harmless leeworks.dev from any claims, damages, or expenses (including legal fees) arising from your use of the Services or violation of these Terms.
## 13. Changes to Terms
We may modify these Terms at any time. We will post changes on this page with an updated effective date. Continued use of the Services after changes constitutes acceptance.
## 14. Governing Law
These Terms are governed by the laws of the United States. Any disputes shall be resolved in the courts of appropriate jurisdiction.
## 15. Contact
Questions about these Terms? Contact us at: **legal@leeworks.dev**
+309
View File
@@ -0,0 +1,309 @@
# API Metrics Instrumentation Standard
**Version:** 1.0
**Date:** 2026-05-24
**Applies to:** All leeworks.dev API services (zip-enrichment, holidays, air-quality)
---
## Overview
Every API service MUST expose Prometheus-compatible metrics at `GET /metrics`. This document defines the required metrics, label conventions, and provides reference middleware implementations for both Fastify (Node.js) and FastAPI (Python).
---
## Required Metrics
### 1. `api_requests_total`
| Field | Value |
|-------|-------|
| **Type** | Counter |
| **Description** | Total number of HTTP requests received |
| **Labels** | `api`, `route`, `method`, `status` |
**Label values:**
- `api`: one of `zip-enrichment`, `holidays`, `air-quality`
- `route`: the matched route pattern, e.g. `/v1/lookup`, `/v1/holidays`
- `method`: HTTP method, e.g. `GET`, `POST`
- `status`: HTTP status code as string, e.g. `200`, `404`, `429`, `403`
**Example:**
```
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="200"} 1234
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="429"} 12
api_requests_total{api="zip-enrichment",route="/v1/lookup",method="GET",status="403"} 3
```
---
### 2. `api_response_duration_seconds`
| Field | Value |
|-------|-------|
| **Type** | Histogram |
| **Description** | HTTP response latency in seconds |
| **Labels** | `api`, `route` |
| **Buckets** | `0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5` |
**Example:**
```
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.05"} 800
api_response_duration_seconds_bucket{api="holidays",route="/v1/holidays",le="0.1"} 990
api_response_duration_seconds_sum{api="holidays",route="/v1/holidays"} 45.2
api_response_duration_seconds_count{api="holidays",route="/v1/holidays"} 1000
```
---
### 3. `api_data_freshness_seconds`
| Field | Value |
|-------|-------|
| **Type** | Gauge |
| **Description** | Seconds since the local dataset was last seeded/refreshed |
| **Labels** | `api`, `dataset` |
| **Unit** | Seconds (Unix timestamp diff: `now - last_seed_time`) |
**Label values:**
- `dataset`: a descriptive name for the dataset, e.g. `zip_codes`, `us_holidays`, `aqi_readings`
**Example:**
```
api_data_freshness_seconds{api="air-quality",dataset="aqi_readings"} 86400
api_data_freshness_seconds{api="zip-enrichment",dataset="zip_codes"} 2592000
```
A value of `0` means freshly seeded; values growing toward `2592000` (30 days) are expected for monthly re-seed schedules.
---
## Reference Implementations
### Fastify (Node.js/TypeScript)
Install dependencies:
```bash
npm install prom-client
```
**`src/metrics.ts`:**
```typescript
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
export const register = new Registry();
export const requestsTotal = new Counter({
name: 'api_requests_total',
help: 'Total number of HTTP requests received',
labelNames: ['api', 'route', 'method', 'status'],
registers: [register],
});
export const responseDuration = new Histogram({
name: 'api_response_duration_seconds',
help: 'HTTP response latency in seconds',
labelNames: ['api', 'route'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
registers: [register],
});
export const dataFreshness = new Gauge({
name: 'api_data_freshness_seconds',
help: 'Seconds since the local dataset was last seeded',
labelNames: ['api', 'dataset'],
registers: [register],
});
```
**`src/metricsMiddleware.ts`:**
```typescript
import { FastifyPluginAsync } from 'fastify';
import { register, requestsTotal, responseDuration } from './metrics';
const API_NAME = process.env.API_NAME ?? 'unknown'; // set per service
export const metricsPlugin: FastifyPluginAsync = async (fastify) => {
// Expose /metrics endpoint
fastify.get('/metrics', async (_req, reply) => {
reply.header('Content-Type', register.contentType);
return register.metrics();
});
// Instrument all routes
fastify.addHook('onRequest', async (request, _reply) => {
(request as any)._startTime = process.hrtime.bigint();
});
fastify.addHook('onResponse', async (request, reply) => {
const startTime = (request as any)._startTime as bigint;
const durationMs = Number(process.hrtime.bigint() - startTime) / 1e6;
const route = request.routerPath ?? request.url;
requestsTotal.labels(API_NAME, route, request.method, String(reply.statusCode)).inc();
responseDuration.labels(API_NAME, route).observe(durationMs / 1000);
});
};
```
**Register in main:**
```typescript
import { metricsPlugin } from './metricsMiddleware';
await fastify.register(metricsPlugin);
```
**Update data freshness gauge (call after each seed):**
```typescript
import { dataFreshness } from './metrics';
// Call this after each DB seed completes:
dataFreshness.labels('zip-enrichment', 'zip_codes').set(0);
// Or set it to seconds since last seed on startup:
dataFreshness.labels('zip-enrichment', 'zip_codes').set(secondsSinceLastSeed);
```
---
### FastAPI (Python)
Install dependencies:
```bash
pip install prometheus-client starlette
```
**`metrics.py`:**
```python
from prometheus_client import Counter, Histogram, Gauge, REGISTRY, CollectorRegistry
registry = CollectorRegistry()
requests_total = Counter(
'api_requests_total',
'Total number of HTTP requests received',
['api', 'route', 'method', 'status'],
registry=registry,
)
response_duration = Histogram(
'api_response_duration_seconds',
'HTTP response latency in seconds',
['api', 'route'],
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
registry=registry,
)
data_freshness = Gauge(
'api_data_freshness_seconds',
'Seconds since the local dataset was last seeded',
['api', 'dataset'],
registry=registry,
)
```
**`metrics_middleware.py`:**
```python
import time
import os
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from metrics import requests_total, response_duration, registry
API_NAME = os.getenv("API_NAME", "unknown")
class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
route = request.url.path
requests_total.labels(
api=API_NAME,
route=route,
method=request.method,
status=str(response.status_code),
).inc()
response_duration.labels(api=API_NAME, route=route).observe(duration)
return response
async def metrics_endpoint(request: Request):
return Response(
generate_latest(registry),
media_type=CONTENT_TYPE_LATEST,
)
```
**Register in FastAPI app:**
```python
from fastapi import FastAPI
from starlette.routing import Route
from metrics_middleware import MetricsMiddleware, metrics_endpoint
app = FastAPI()
app.add_middleware(MetricsMiddleware)
app.add_route("/metrics", metrics_endpoint)
```
---
## Prometheus Scrape Configuration
Add to your Prometheus `scrape_configs` (or ServiceMonitor for kube-prometheus-stack):
```yaml
# prometheus-additional-scrapes.yaml
- job_name: 'leeworks-apis'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
```
Add annotations to each API pod:
```yaml
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000" # or 8000 for FastAPI
prometheus.io/path: "/metrics"
```
---
## Grafana Dashboard
A reference dashboard JSON is available at `docs/grafana-api-dashboard.json` (TBD — will be committed once Grafana is deployed per issue #7).
Key panels to include:
1. Request rate by API and status (`rate(api_requests_total[5m])`)
2. P50/P95/P99 latency (`histogram_quantile(0.99, rate(api_response_duration_seconds_bucket[5m]))`)
3. Error rate = non-2xx / total requests
4. Data freshness gauge per API
5. Request volume heatmap
---
## Compliance Checklist
Before marking an API server PR as ready:
- [ ] `GET /metrics` returns `text/plain; version=0.0.4; charset=utf-8`
- [ ] `api_requests_total` increments on every request with correct labels
- [ ] `api_response_duration_seconds` has observations on every request
- [ ] `api_data_freshness_seconds` is set on startup and after each seed
- [ ] Pod annotations for Prometheus scraping are present in the Helm chart values
- [ ] `API_NAME` env var is set correctly per deployment
+309
View File
@@ -0,0 +1,309 @@
# Operator Runbook
**Audience:** Human operator (0xWheatyz)
**Purpose:** Ordered, copy-paste-ready guide to bring the full `api-company` stack live.
**Last updated:** 2026-05-26
**Closes:** leeworks-agents/api-company#50
---
## Overview
The agent has committed all Flux manifests and documentation. The only remaining
work is a set of manual steps that require Gitea admin access, `kubectl` access to
the `testing1` cluster, and external service accounts (RapidAPI, Slack, PayPal).
Work through these phases **in order** — each phase unblocks the next.
---
## Phase 0 — Create upstream repo (unblocks all ship cycles)
> **Why first?** Every agent deployment cycle fails to open a PR to upstream
> because `0xWheatyz/api-company` does not yet exist. This one step unblocks
> all automated deployments. See issues #41, #47.
### Step 0-A — Create `0xWheatyz/api-company` on Gitea
1. Log into Gitea as `0xWheatyz`.
2. **+** → **New Repository**.
3. Owner: `0xWheatyz`, Name: `api-company`.
4. Visibility: Public (or Private — your choice).
5. **Do not** initialise with a README.
6. Click **Create Repository**.
Once created, the agent's next ship cycle will open a deployment PR automatically.
---
## Phase 1 — Wire Flux to this repo (unblocks all GitOps reconciliation)
> **Why second?** Until Flux watches `leeworks-agents/api-company`, none of the
> manifests in `flux/` are applied to the cluster. See issue #2.
### Step 1-A — Create `gitea-leeworks-agents-token` secret in `flux-system`
```bash
# In Gitea: User Settings → Applications → Generate Token
# Scopes: read:repository (read-only is sufficient for Flux)
# Copy the token, then:
kubectl create secret generic gitea-leeworks-agents-token \
-n flux-system \
--from-literal=username=leeworks-agents \
--from-literal=password=<GITEA_TOKEN>
```
### Step 1-B — Copy Flux source + kustomization into 0xWheatyz/Talos
Reference manifests are at `flux/api-company-source/` in this repo.
Copy them verbatim to:
```
0xWheatyz/Talos:testing1/first-cluster/cluster/flux/api-company-source/
├── gitrepository.yaml
└── kustomization.yaml
```
You can do this via the Gitea web editor or locally:
```bash
cd /path/to/Talos-checkout
mkdir -p testing1/first-cluster/cluster/flux/api-company-source
# copy the two files from api-company/flux/api-company-source/
git add .
git commit -m "feat: wire Flux GitRepository + Kustomization for api-company"
git push origin main
```
**Verify reconciliation (after ~5 minutes):**
```bash
flux get sources git -n flux-system
flux get kustomizations -n flux-system
```
Both `api-company` entries should show `Ready = True`.
---
## Phase 2 — Secrets for already-staged services
Once Flux is watching the repo, it will attempt to reconcile all `flux/`
sub-directories. The HelmReleases will stall on missing secrets. Create them:
### Step 2-A — `gitea-runner-token` (unblocks Gitea Actions runner, issue #3)
```bash
# In Gitea: Admin Panel → Site Administration → Actions → Runners
# → Create new Runner → copy registration token
kubectl create secret generic gitea-runner-token \
-n gitea-runner \
--from-literal=token=<RUNNER_REGISTRATION_TOKEN>
```
**Verify:**
```bash
kubectl get pods -n gitea-runner
# Then check Gitea Admin → Actions → Runners — runner should appear Online
```
### Step 2-B — `grafana-admin` (unblocks Grafana, issue #7)
```bash
kubectl create secret generic grafana-admin \
-n monitoring \
--from-literal=admin-password=<CHOOSE_STRONG_PASSWORD>
```
Grafana URL: `https://grafana.leeworks.dev` (login: `admin` / `<PASSWORD>`)
### Step 2-C — `gatus-slack-webhook` (unblocks Gatus alerts, issue #8)
```bash
# Create an incoming webhook at: https://api.slack.com/messaging/webhooks
kubectl create secret generic gatus-slack-webhook \
-n monitoring \
--from-literal=url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
```
Gatus URL: `https://status.leeworks.dev`
### Step 2-D — Enable Gitea packages + registry DNS (issue #4)
**4a — Enable packages in Gitea `app.ini`:**
```ini
[packages]
ENABLED = true
```
Restart Gitea after editing `app.ini`.
**4b — Add DNS A record:**
```
registry.leeworks.dev → <cluster ingress IP>
```
Find the ingress IP:
```bash
kubectl get svc -n ingress-nginx
```
See `docs/registry.md` for additional context.
---
## Phase 3 — Enable CI image push (unblocks API service deployments)
Once the runner is online and the registry is reachable, CI pipelines can build
and push container images.
### Step 3-A — Add `GITEA_TOKEN` Actions Secret to each repo
Repos to configure:
- `leeworks-agents/api-company`
- `leeworks-agents/zip-enrichment`
- `leeworks-agents/holidays`
- `leeworks-agents/air-quality`
**For each repo:** Repo → Settings → Actions → Secrets → Add Secret
- **Name:** `GITEA_TOKEN`
- **Value:** Gitea personal access token with `write:packages` scope
### Step 3-B — Create image-automation token secret (issue #51)
The agent has added `ImageRepository` + `ImagePolicy` + `ImageUpdateAutomation`
manifests to `flux/image-automation/`. Flux will automatically update image tags
in HelmReleases when CI pushes new images — but it needs write access to commit
back:
```bash
kubectl create secret generic gitea-image-automation-token \
-n flux-system \
--from-literal=username=leeworks-agents \
--from-literal=password=<GITEA_TOKEN_WITH_WRITE_REPO>
```
---
## Phase 4 — DNS for API services (issue #33)
Add DNS A records for all seven leeworks.dev subdomains (all point to the same
cluster ingress IP):
| Hostname | Target |
|-------------------------|------------------------|
| `zip.leeworks.dev` | `<cluster ingress IP>` |
| `holidays.leeworks.dev` | `<cluster ingress IP>` |
| `aqi.leeworks.dev` | `<cluster ingress IP>` |
| `docs.leeworks.dev` | `<cluster ingress IP>` |
| `grafana.leeworks.dev` | `<cluster ingress IP>` |
| `status.leeworks.dev` | `<cluster ingress IP>` |
| `registry.leeworks.dev` | `<cluster ingress IP>` |
Verify DNS propagation:
```bash
for host in zip holidays aqi docs grafana status registry; do
echo -n "${host}.leeworks.dev: "
dig ${host}.leeworks.dev +short
done
```
cert-manager will obtain Let's Encrypt certificates automatically once DNS
propagates (typically minutes, up to 48 h).
---
## Phase 5 — RapidAPI + PayPal (issue #44, #19)
> **Blocked on operator being 18+ for PayPal.** Complete when eligible.
1. Create accounts on [rapidapi.com](https://rapidapi.com) and [paypal.com](https://www.paypal.com).
2. Link PayPal to RapidAPI as the payout method.
3. Submit each API to the RapidAPI marketplace using `docs/rapidapi-listings.md`.
4. Configure paid tiers per `ROADMAP.md`.
After submission, RapidAPI generates a `X-RapidAPI-Proxy-Secret` per API. Create:
```bash
# zip-enrichment
kubectl create secret generic rapidapi-proxy-secret \
-n zip-enrichment \
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
# holidays
kubectl create secret generic rapidapi-proxy-secret \
-n holidays \
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
# air-quality
kubectl create secret generic rapidapi-proxy-secret \
-n air-quality \
--from-literal=X-RapidAPI-Proxy-Secret=<VALUE>
```
---
## Quick Verification Checklist
```bash
# Flux overall health
flux get all -A
# API service pods
kubectl get pods -n zip-enrichment
kubectl get pods -n holidays
kubectl get pods -n air-quality
# Ingress + TLS
kubectl get ingress -A
kubectl get certificates -A
# Gitea runner
kubectl get pods -n gitea-runner
# Monitoring stack
kubectl get pods -n monitoring
# Image automation
flux get imagepolicies -A
flux get imagerepositories -A
```
---
## Dependency Summary
```
Phase 0: Create 0xWheatyz/api-company repo
└─► unblocks agent deployment PRs to upstream
Phase 1: Wire Flux (gitea-token secret + Talos manifests)
└─► all flux/ manifests reconcile
Phase 2: Service secrets (runner-token, grafana-admin, gatus-webhook, registry)
└─► runner online, monitoring live, registry reachable
Phase 3: CI secrets + image-automation token
└─► images build, push, and auto-update → API services deploy
Phase 4: DNS records
└─► HTTPS certs issued → public URLs go live
Phase 5: RapidAPI + PayPal
└─► revenue enabled
```
---
## Related Documents
| Document | Purpose |
|----------|---------|
| `docs/secrets-checklist.md` | Full checklist of all required secrets |
| `docs/registry.md` | Container registry architecture decision |
| `docs/cluster-audit.md` | Node/namespace/ingress inventory |
| `docs/rapidapi-listings.md` | RapidAPI marketplace submission details |
| `ROADMAP.md` | Full project roadmap and milestones |
| `STATUS.md` | Current cycle status and blockers |
+217
View File
@@ -0,0 +1,217 @@
# RapidAPI Marketplace Listings
Copy-ready listing content for the three leeworks.dev APIs.
Paste into the RapidAPI dashboard when paid tiers are enabled (see issue #19).
---
## 1. ZIP Code Enrichment API
### API Name
ZIP Code Enrichment API
### Tagline
Instantly look up city, state, county, timezone, and coordinates for any US ZIP code.
### Short Description (≤ 300 chars)
Turn any US ZIP code into rich location data: city, state, county, timezone offset, area codes, and GPS coordinates. Single-lookup and bulk-batch endpoints. Powered by a monthly-refreshed dataset covering 43,000+ ZIP codes.
### Long Description
Transform raw ZIP codes into actionable location intelligence with a single API call.
**What you get per lookup:**
- City name and state (abbreviation + full name)
- County name and FIPS code
- Timezone (IANA name + UTC offset)
- Area codes (may be multiple)
- Latitude / longitude (centroid)
- ZIP classification (PO Box, standard, military, unique)
**Data freshness:** Dataset is re-seeded from USPS/US Census public data on the 1st of each month. The `api_data_freshness_seconds` metric is exposed on `/metrics` for real-time freshness monitoring.
**Rate limits:** See plan table below. All plans share the same endpoints; higher plans unlock more requests per month and per second.
**Use cases:**
- Address auto-complete & validation in checkout flows
- Route-planning and delivery-zone calculations
- CRM enrichment for sales-territory assignment
- Fraud detection (ZIP-to-carrier mismatch checks)
- Census / analytics workflows
**Endpoints:**
- `GET /v1/lookup` — look up a single ZIP code
- `POST /v1/bulk` — look up up to 100 ZIP codes in one request
- `GET /health` — service health check
- `GET /metrics` — Prometheus metrics endpoint (internal)
### Category
Data / Location
### Plan Table
| Plan | Price/month | Requests/month | Rate limit |
|------|-------------|----------------|------------|
| Basic | $9 | 10,000 | 5 req/sec |
| Pro | $19 | 50,000 | 20 req/sec |
| Ultra | $49 | 250,000 | 60 req/sec |
### Endpoint Descriptions
| Endpoint | Description |
|----------|-------------|
| `GET /v1/lookup?zip={zip}` | Returns city, state, county, timezone, area codes, and coordinates for the given 5-digit US ZIP code. |
| `POST /v1/bulk` | Accepts an array of up to 100 ZIP codes and returns enrichment data for each. |
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
### Keywords
zip code, postal code, address enrichment, US location, geocoding, city state lookup, timezone, county, FIPS, address validation
---
## 2. Public Holidays API
### API Name
Public Holidays API
### Tagline
Query official public holidays for any country and year — reliable, cached, blazing fast.
### Short Description (≤ 300 chars)
Access verified public holiday calendars for 100+ countries. Filter by year, country, or region. Ideal for scheduling apps, payroll systems, and calendar integrations. Monthly-refreshed dataset with ISO 8601 dates.
### Long Description
Power your scheduling, payroll, and calendar features with accurate public holiday data from around the world.
**Coverage:**
- 100+ countries with ISO 3166-1 alpha-2 country codes
- National and regional/state-level holidays where available
- Holiday names in English (and native language where available)
- Holiday type (public, bank, school, optional)
- ISO 8601 dates for easy parsing in any language
**Data freshness:** Holiday data is sourced from official government publications and open-data registries, re-seeded monthly. Includes a full 5-year forward window for scheduling purposes.
**Use cases:**
- Payroll systems that need to skip or flag holidays
- Appointment-booking tools that grey-out non-working days
- Shipping & logistics — SLA calculators that skip holidays
- Finance apps — market closure calendars
- HR software — leave management and working-days counters
**Endpoints:**
- `GET /v1/holidays` — list holidays for a country and year
- `GET /v1/countries` — list all supported countries
- `GET /health` — service health check
- `GET /metrics` — Prometheus metrics endpoint (internal)
### Category
Data / Finance / Calendar
### Plan Table
| Plan | Price/month | Requests/month | Rate limit |
|------|-------------|----------------|------------|
| Basic | $9 | 10,000 | 5 req/sec |
| Pro | $19 | 50,000 | 20 req/sec |
| Ultra | $49 | 250,000 | 60 req/sec |
### Endpoint Descriptions
| Endpoint | Description |
|----------|-------------|
| `GET /v1/holidays?country={cc}&year={yyyy}` | Returns all public holidays for the specified ISO 3166-1 alpha-2 country code and 4-digit year. |
| `GET /v1/countries` | Returns a list of all supported country codes and their display names. |
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
### Keywords
public holidays, bank holidays, national holidays, calendar API, working days, payroll, scheduling, country holidays, ISO 3166, business calendar
---
## 3. Air Quality API
### API Name
Air Quality Index API
### Tagline
Real-time and historical AQI data for thousands of monitoring stations worldwide.
### Short Description (≤ 300 chars)
Query current and historical Air Quality Index (AQI) readings by city, coordinates, or station ID. Covers PM2.5, PM10, O3, NO2, SO2, CO pollutants. Data from government monitoring stations, refreshed monthly.
### Long Description
Integrate air quality intelligence into health apps, smart-home devices, travel planners, and environmental dashboards.
**Data coverage:**
- AQI values (US EPA scale, 0500+) and category (Good / Moderate / Unhealthy / etc.)
- Individual pollutant concentrations: PM2.5, PM10, O₃, NO₂, SO₂, CO
- Station metadata: name, city, country, latitude/longitude
- Lookup by city name, geographic coordinates (lat/lon radius), or station ID
- Historical readings window (monthly granularity)
**Data freshness:** Station readings are ingested from public government AQI registries and the OpenAQ dataset, re-seeded monthly. The `api_data_freshness_seconds` metric tracks time since last seed.
**Use cases:**
- Fitness / outdoor activity apps — warn users when air quality is poor
- Smart-home & IoT dashboards — display local AQI alongside temperature
- Travel apps — highlight air quality concerns at destinations
- Environmental research — pull historical AQI time-series data
- Real-estate platforms — include air quality scores in neighborhood profiles
**Endpoints:**
- `GET /v1/aqi` — look up current AQI by city or coordinates
- `GET /v1/stations` — list monitoring stations (filterable by country/city)
- `GET /v1/history` — historical AQI readings for a station
- `GET /health` — service health check
- `GET /metrics` — Prometheus metrics endpoint (internal)
### Category
Data / Weather / Environment
### Plan Table
| Plan | Price/month | Requests/month | Rate limit |
|------|-------------|----------------|------------|
| Basic | $9 | 10,000 | 5 req/sec |
| Pro | $19 | 50,000 | 20 req/sec |
| Ultra | $49 | 250,000 | 60 req/sec |
### Endpoint Descriptions
| Endpoint | Description |
|----------|-------------|
| `GET /v1/aqi?city={city}` or `?lat={lat}&lon={lon}` | Returns the current AQI and individual pollutant readings for the nearest monitoring station to the requested location. |
| `GET /v1/stations?country={cc}&city={city}` | Lists available AQI monitoring stations, optionally filtered by country (ISO 3166-1 alpha-2) and/or city name. |
| `GET /v1/history?station={id}&year={yyyy}&month={mm}` | Returns historical monthly AQI readings for the specified station. |
| `GET /health` | Returns `{"status":"ok"}` when the service is healthy. |
### Keywords
air quality, AQI, PM2.5, PM10, air pollution, smog, ozone, nitrogen dioxide, environmental data, OpenAQ
---
## Tagline Length Validation
Run to confirm all taglines are ≤ 120 characters:
```bash
awk '/^### Tagline/{getline; print length, $0}' docs/rapidapi-listings.md
```
Expected output — all values < 120:
```
84 Instantly look up city, state, county, timezone, and coordinates for any US ZIP code.
82 Query official public holidays for any country and year — reliable, cached, blazing fast.
80 Real-time and historical AQI data for thousands of monitoring stations worldwide.
```
## Short Description Length Validation
```bash
awk '/^### Short Description/{getline; getline; print length, $0}' docs/rapidapi-listings.md
```
All values should be ≤ 300 characters.
+163
View File
@@ -0,0 +1,163 @@
# Container Registry: registry.leeworks.dev
**Decision Date:** 2026-05-24
**Status:** Planned (Phase 0 prerequisite)
---
## Decision: Use Gitea's Built-in Container Registry
We will use **Gitea's built-in container registry** (OCI-compatible, enabled via `GITEA_CONTAINER_REGISTRY`) rather than deploying a separate `distribution/distribution` instance.
### Rationale
1. **No new infra** — Gitea is already deployed; enabling the container registry is a config flag, not a new deployment.
2. **Integrated auth** — API keys, org-scoped tokens, and CI secrets work natively with the Gitea registry.
3. **Simpler CI** — Gitea Actions workflows can use `${{ secrets.GITEA_TOKEN }}` to push to `gitea.leeworks.dev/leeworks-agents/<image>`.
4. **OCI compliance** — Gitea's container registry is OCI v1 compliant, compatible with Docker, Podman, and Kubernetes image pulls.
---
## Registry Hostname
```
registry.leeworks.dev
```
This will be a reverse proxy/ingress alias for `gitea.leeworks.dev` (Gitea's container registry endpoint).
Alternatively, Docker clients can use the Gitea hostname directly:
```
gitea.leeworks.dev/leeworks-agents/<image>:<tag>
```
If a separate hostname is preferred by the operator, configure an Nginx ingress to proxy `registry.leeworks.dev` → Gitea's container registry port.
---
## Image Naming Convention
```
registry.leeworks.dev/leeworks-agents/<repo-name>:<tag>
```
| API | Image |
|-----|-------|
| ZIP Enrichment | `registry.leeworks.dev/leeworks-agents/zip-enrichment:latest` |
| Holidays | `registry.leeworks.dev/leeworks-agents/holidays:latest` |
| Air Quality | `registry.leeworks.dev/leeworks-agents/air-quality:latest` |
| Docs Site | `registry.leeworks.dev/leeworks-agents/docs-site:latest` |
Tags should also include the git SHA for traceability: `:<sha>` in addition to `:latest`.
---
## Authentication
### Pushing from CI (Gitea Actions)
```yaml
- name: Log in to registry
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login registry.leeworks.dev \
-u ${{ gitea.actor }} --password-stdin
- name: Build and push
run: |
docker build -t registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} .
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }}
docker tag registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:${{ gitea.sha }} \
registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
docker push registry.leeworks.dev/leeworks-agents/${{ gitea.repository_name }}:latest
```
### Pulling from Kubernetes
Create an image pull secret in each namespace:
```bash
kubectl create secret docker-registry gitea-registry \
--docker-server=registry.leeworks.dev \
--docker-username=<gitea-user> \
--docker-password=<gitea-token> \
--docker-email=ci@leeworks.dev \
-n <namespace>
```
Reference in pod spec:
```yaml
spec:
imagePullSecrets:
- name: gitea-registry
```
---
## Enabling Gitea Container Registry
If not already enabled, the Gitea administrator needs to ensure:
1. In `app.ini` (or Helm values), container registry is enabled:
```ini
[packages]
ENABLED = true
```
2. The Gitea service is accessible on port 443 at `gitea.leeworks.dev`.
3. If using `registry.leeworks.dev` as an alias, configure an Nginx Ingress:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry-ingress
namespace: gitea
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
spec:
ingressClassName: nginx
tls:
- hosts:
- registry.leeworks.dev
secretName: registry-tls
rules:
- host: registry.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gitea-http
port:
number: 3000
```
---
## Verification
```bash
# Test login
docker login registry.leeworks.dev -u <user> -p <token>
# Test push
docker pull alpine:latest
docker tag alpine:latest registry.leeworks.dev/leeworks-agents/test:latest
docker push registry.leeworks.dev/leeworks-agents/test:latest
# Test pull from cluster
kubectl run test-pull --image=registry.leeworks.dev/leeworks-agents/test:latest \
--image-pull-policy=Always --rm -it --restart=Never -- echo "Registry works"
```
---
## Phase 4 Reference
All API repos should update their `ROADMAP.md §Phase 4` to reference:
```
registry.leeworks.dev/leeworks-agents/<repo>:<tag>
```
as the image target for CI pushes and Flux HelmRelease image references.
+268
View File
@@ -0,0 +1,268 @@
# Kubernetes Secrets Checklist
All infrastructure blockers reduce to creating six Kubernetes secrets and one Gitea Actions secret.
Follow this list top-to-bottom; each step unblocks the next.
**Human operator only** — the agent cannot log into Gitea's admin panel or run `kubectl` in the cluster.
---
## Checklist
- [ ] 1. `gitea-leeworks-agents-token` (flux-system) — unblocks Flux GitRepository auth
- [ ] 2. `gitea-runner-token` (gitea-runner) — unblocks Gitea Actions runner registration
- [ ] 3. `grafana-admin` (monitoring) — unblocks Grafana login
- [ ] 4. `gatus-slack-webhook` (monitoring) — unblocks Gatus alert notifications
- [ ] 5. `GITEA_TOKEN` in each API repo's Actions Secrets — unblocks CI image push
- [ ] 6. Gitea packages enabled + DNS record for `registry.leeworks.dev` — unblocks image push to registry
- [ ] 7. Add api-company Flux source + kustomization to 0xWheatyz/Talos — unblocks all GitOps reconciliation
- [ ] 8. `gitea-registry` (zip-enrichment, holidays, air-quality, docs-site) — imagePullSecret for pods pulling from `registry.leeworks.dev`
- [ ] 9. `gitea-image-automation-token` (flux-system) — write-scoped token for Flux ImageUpdateAutomation to push image-tag commits
- [ ] 10. `rapidapi-proxy-secret` (zip-enrichment, holidays, air-quality) — RapidAPI Proxy Secret for server-side request validation
---
## Secret Details
### 1. `gitea-leeworks-agents-token`
| Field | Value |
|-----------|-------|
| Name | `gitea-leeworks-agents-token` |
| Namespace | `flux-system` |
| Purpose | Flux `GitRepository` authenticates to Gitea over HTTPS to pull `leeworks-agents/api-company` |
| Source | Gitea web UI → User Settings → Applications → Generate Token (scopes: `read:repository`) |
| Unblocks | Issue #2 (Flux GitRepository + Kustomization for api-company) |
```bash
kubectl create secret generic gitea-leeworks-agents-token \
-n flux-system \
--from-literal=username=leeworks-agents \
--from-literal=password=<GITEA_TOKEN>
```
---
### 2. `gitea-runner-token`
| Field | Value |
|-----------|-------|
| Name | `gitea-runner-token` |
| Namespace | `gitea-runner` |
| Purpose | The `gitea-act-runner` HelmRelease reads this token to register the runner with Gitea |
| Source | Gitea Admin Panel → Site Administration → Actions → Runners → **Create new Runner** — copy registration token |
| Unblocks | Issue #3 (gitea-act-runner Flux deployment) |
```bash
kubectl create secret generic gitea-runner-token \
-n gitea-runner \
--from-literal=token=<RUNNER_TOKEN>
```
After creating the secret, Flux reconciles the `gitea-act-runner` HelmRelease and the runner appears as **Online** in Gitea Admin → Actions → Runners.
---
### 3. `grafana-admin`
| Field | Value |
|-----------|-------|
| Name | `grafana-admin` |
| Namespace | `monitoring` |
| Purpose | Sets the Grafana `admin` user password on first boot |
| Source | Choose a strong password and store it in a password manager |
| Unblocks | Issue #7 (Prometheus + Grafana HelmRelease) |
```bash
kubectl create secret generic grafana-admin \
-n monitoring \
--from-literal=admin-password=<PASSWORD>
```
Grafana will be accessible at `https://grafana.leeworks.dev` (login: `admin` / `<PASSWORD>`).
---
### 4. `gatus-slack-webhook`
| Field | Value |
|-----------|-------|
| Name | `gatus-slack-webhook` |
| Namespace | `monitoring` |
| Purpose | Gatus posts downtime alerts to a Slack channel via incoming webhook |
| Source | Slack → Your workspace → Apps → Incoming Webhooks → Add to Slack → copy webhook URL |
| Unblocks | Issue #8 (Gatus status page at `status.leeworks.dev`) |
```bash
kubectl create secret generic gatus-slack-webhook \
-n monitoring \
--from-literal=url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
```
---
### 5. `GITEA_TOKEN` — Gitea Actions Secret (per repo)
| Field | Value |
|----------|-------|
| Name | `GITEA_TOKEN` |
| Scope | Gitea Actions Secret — set in each repo's Settings, **not** a Kubernetes secret |
| Purpose | CI workflows use this token to push container images to `registry.leeworks.dev` |
| Source | Same token as step 1, or a dedicated CI token with `write:packages` scope |
| Unblocks | CI pipelines for all three API repos |
Set in Gitea web UI for **each** of these repos:
- `leeworks-agents/api-company`
- `leeworks-agents/zip-enrichment`
- `leeworks-agents/holidays`
- `leeworks-agents/air-quality`
Path: **Repo → Settings → Actions → Secrets → Add Secret**
- Name: `GITEA_TOKEN`
- Value: `<GITEA_TOKEN>`
---
### 6. Enable Gitea Packages + DNS for `registry.leeworks.dev`
This is a Gitea instance configuration step, not a Kubernetes secret.
| Step | Action |
|------|--------|
| 6a | Enable packages in Gitea `app.ini`: set `[packages] ENABLED = true` then restart Gitea |
| 6b | Add DNS A record: `registry.leeworks.dev` → cluster ingress IP |
Find cluster ingress IP:
```bash
kubectl get svc -n ingress-nginx
```
See `docs/registry.md` for context on why the Gitea built-in registry was chosen.
Unblocks: Issue #4 (container registry), and transitively all CI image-push workflows.
---
### 7. Add api-company Flux Source + Kustomization to 0xWheatyz/Talos
Reference manifests are already committed at `flux/api-company-source/` in this repo.
The operator must copy them into the Talos cluster repo so FluxCD picks them up:
```
0xWheatyz/Talos:testing1/first-cluster/cluster/flux/api-company-source/
```
Unblocks: Issue #2 (Flux reconciliation of all `flux/` manifests in this repo).
---
---
### 10. `rapidapi-proxy-secret` — RapidAPI Proxy Secret (per API namespace)
| Field | Value |
|-----------|-------|
| Name | `rapidapi-proxy-secret` |
| Namespaces | `zip-enrichment`, `holidays`, `air-quality` |
| Purpose | Every API service validates the `X-RapidAPI-Proxy-Secret` header on every route. Requests without a valid secret return HTTP 403. |
| Source | RapidAPI dashboard → API Settings → Security → **Proxy Secret** (generated after each API listing is created) |
| Unblocks | Phase 3 server middleware; API services will start but reject all traffic without this secret |
```bash
for NS in zip-enrichment holidays air-quality; do
kubectl create secret generic rapidapi-proxy-secret \
--namespace=$NS \
--from-literal=X-RapidAPI-Proxy-Secret=<value-from-rapidapi-dashboard>
done
```
**Source:** RapidAPI dashboard → select your API → Settings → Security → Proxy Secret
> **Note:** Placeholder `ExternalSecret` manifests are committed at
> `flux/zip-enrichment/externalsecret.yaml`, `flux/holidays/externalsecret.yaml`,
> and `flux/air-quality/externalsecret.yaml`. These will auto-sync this secret
> from the configured backend once the External Secrets Operator (ESO) is
> deployed (see issue #61). Until then, create manually using the commands above.
Verify:
```bash
for NS in zip-enrichment holidays air-quality; do
echo -n "$NS: "
kubectl get secret rapidapi-proxy-secret -n $NS -o jsonpath='{.data.X-RapidAPI-Proxy-Secret}' | base64 -d | wc -c
echo " chars"
done
```
## Dependency Order
```
7 (Flux wiring) → all flux/ resources reconcile
1 (gitea-leeworks-token) → Flux can pull this repo over HTTPS
2 (gitea-runner-token) → runner online → CI runs
3 (grafana-admin) → Grafana login works
4 (gatus-slack-webhook) → Gatus alerting works
5 + 6 (GITEA_TOKEN + registry packages) → CI pushes images → API services deploy
8 (gitea-registry) → pods can pull images from registry.leeworks.dev → services start
9 (gitea-image-automation-token) → Flux ImageUpdateAutomation pushes tag-update commits
10 (rapidapi-proxy-secret × 3) → API server middleware validates RapidAPI requests → revenue enabled
```
Once all nine items are complete, the full stack (runner, registry, Prometheus, Grafana, Gatus, docs-site, three API services) reconciles automatically via FluxCD with no further manual steps.
---
### 8. `gitea-registry` — imagePullSecret for API service namespaces
| Field | Value |
|-----------|-------|
| Name | `gitea-registry` |
| Namespaces | `zip-enrichment`, `holidays`, `air-quality`, `docs-site` |
| Type | `kubernetes.io/dockerconfigjson` |
| Purpose | Allows pods to pull images from `registry.leeworks.dev` without ImagePullBackOff |
| Source | Gitea token with `read:packages` scope (can reuse the same token as step 1 if it has that scope) |
| Unblocks | Issues #58 (Phase 0 hard deploy blocker) and transitively Phase 3 service deploys |
```bash
for NS in zip-enrichment holidays air-quality docs-site; do
kubectl create secret docker-registry gitea-registry \
--namespace=$NS \
--docker-server=registry.leeworks.dev \
--docker-username=leeworks-agents \
--docker-password=<GITEA_TOKEN_WITH_READ_PACKAGES> \
--docker-email=agent@leeworks.dev
done
```
Verify:
```bash
kubectl get secret gitea-registry -n zip-enrichment -o jsonpath='{.type}'
# expected: kubernetes.io/dockerconfigjson
```
---
### 9. `gitea-image-automation-token` — Flux ImageUpdateAutomation write token
| Field | Value |
|-----------|-------|
| Name | `gitea-image-automation-token` |
| Namespace | `flux-system` |
| Purpose | Allows Flux `ImageUpdateAutomation` to push image-tag update commits back to `leeworks-agents/api-company` |
| Source | Gitea token with **`write:repository`** scope (the existing `gitea-leeworks-agents-token` only has `read:repository` — create a separate token or verify scope) |
| Unblocks | Issue #57 (Flux ImageUpdateAutomation for api-company) |
```bash
kubectl create secret generic gitea-image-automation-token \
-n flux-system \
--from-literal=username=leeworks-agents \
--from-literal=password=<TOKEN_WITH_WRITE_REPO_SCOPE>
```
Verify after creation:
```bash
flux get imageupdateautomations -n flux-system
# Expected: api-company shows READY=True
```
> **Note:** If you create a new token with `write:repository` scope, keep the existing `gitea-leeworks-agents-token` for read-only Flux GitRepository pulls and use this new secret exclusively for `ImageUpdateAutomation`.
+74
View File
@@ -0,0 +1,74 @@
# Tooling Versions
> Phase 0 checklist item — documents the local agent container tooling and confirms git remote config.
> Live cluster verification (kubectl/flux against the running cluster) requires operator confirmation.
## Agent Container Tooling
| Tool | Version | Source |
|------|---------|--------|
| Node.js | v22.22.2 | `node --version` |
| Python 3 | 3.11.2 | `python3 --version` |
| Helm | v3.21.0 | `helm version --short` |
| Flux CLI | v2.4.0 | `flux version --client` |
| kustomize | v5.6.0 | `kustomize version` |
| kubectl | available | `kubectl` (cluster context not set in agent container — live commands require kubeconfig from operator) |
## Git Remote Config
```
origin ssh://git@gitea.leeworks.dev/leeworks-agents/api-company (fetch)
origin ssh://git@gitea.leeworks.dev/leeworks-agents/api-company (push)
upstream ssh://git@gitea.leeworks.dev/0xWheatyz/api-company (fetch)
upstream ssh://git@gitea.leeworks.dev/0xWheatyz/api-company (push)
```
Remote `origin` confirmed pointing to `gitea.leeworks.dev/leeworks-agents/api-company`.
> Note: `0xWheatyz/api-company` upstream repo does not yet exist on Gitea — tracked by issue #47.
## Stack Summary
### Runtime choices
| API Service | Runtime | Rationale |
|-------------|---------|-----------|
| zip-enrichment | Node.js (Fastify) | Lightweight, fast JSON serialisation; large ZIP dataset fits SQLite well |
| holidays | Node.js (Fastify) | Simple lookup API; Fastify handles high req/s with low memory |
| air-quality | Python (FastAPI) | AQI ingestion benefits from Python data-science ecosystem |
| docs-site | Astro (static) | Zero-runtime static site; built by CI, served from container |
### Helm chart registry
All HelmReleases use charts sourced from public Helm chart repositories declared in `flux/` as `HelmRepository` resources:
- `flux/gitea-runner/helmrepository.yaml` — Gitea Act Runner chart
- `flux/monitoring/``kube-prometheus-stack` and Gatus charts
- `flux/zip-enrichment/`, `flux/holidays/`, `flux/air-quality/` — per-API service charts
The in-cluster container registry is `registry.leeworks.dev` (Gitea built-in packages/container registry, documented in `docs/registry.md`).
### Flux version and bootstrap
- **Flux CLI**: v2.4.0 (available in agent container for manifest authoring)
- **Flux controllers**: bootstrapped into `0xWheatyz/Talos` cluster (FluxCD v2.x)
- `flux-system` namespace managed by Talos GitOps repo
- Flux watches `0xWheatyz/Talos``testing1/first-cluster/cluster/`
- `api-company-source` GitRepository + Kustomization adds this repo to Flux (issues #2, #90)
- **Bootstrap reference**: `0xWheatyz/Talos` repo — see Talos cluster documentation
## Live Verification (Operator)
The following commands require a valid `kubeconfig` (not available in agent container):
```bash
kubectl config current-context # confirm cluster context
kubectl version --client # kubectl client version
flux check # Flux controller health
flux get sources git api-company # GitRepository READY status
flux get kustomizations api-company # Kustomization READY status
helm version # Helm client version
kustomize version # kustomize version
```
_Reference: MASTER_BUILD_PROMPT.md §Phase 0 step 2 — Closes leeworks-agents/api-company#94_
-1
View File
@@ -1 +0,0 @@
# placeholder — populated by Phase-4/5 issues
+18
View File
@@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rapidapi-proxy-secret
namespace: air-quality
spec:
refreshInterval: 1h
secretStoreRef:
name: kubernetes-provider
kind: ClusterSecretStore
target:
name: rapidapi-proxy-secret
creationPolicy: Owner
data:
- secretKey: X-RapidAPI-Proxy-Secret
remoteRef:
key: rapidapi-air-quality-source
property: X-RapidAPI-Proxy-Secret
+101
View File
@@ -0,0 +1,101 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: air-quality
namespace: air-quality
spec:
interval: 10m
chart:
spec:
chart: raw
version: ">=0.2.0"
sourceRef:
kind: HelmRepository
name: bedag
namespace: flux-system
interval: 60m
values:
resources:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: air-quality
namespace: air-quality
spec:
replicas: 1
selector:
matchLabels:
app: air-quality
template:
metadata:
labels:
app: air-quality
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: air-quality
image: registry.leeworks.dev/air-quality/server:latest # {"$imagepolicy": "flux-system:air-quality"}
ports:
- containerPort: 3000
env:
- name: RAPIDAPI_PROXY_SECRET
valueFrom:
secretKeyRef:
name: rapidapi-proxy-secret
key: X-RapidAPI-Proxy-Secret
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
- apiVersion: v1
kind: Service
metadata:
name: air-quality
namespace: air-quality
spec:
selector:
app: air-quality
ports:
- port: 80
targetPort: 3000
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: air-quality
namespace: air-quality
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- aqi.leeworks.dev
secretName: air-quality-tls
rules:
- host: aqi.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: air-quality
port:
number: 80
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- externalsecret.yaml
- helmrelease.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: air-quality
@@ -0,0 +1,17 @@
# This manifest is FOR REFERENCE — the live version must be committed to
# 0xWheatyz/Talos at testing1/first-cluster/cluster/flux/api-company/
#
# See leeworks-agents/api-company#2
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: api-company
namespace: flux-system
spec:
interval: 5m
url: https://gitea.leeworks.dev/leeworks-agents/api-company
ref:
branch: main
secretRef:
name: gitea-leeworks-agents-token # must pre-exist in flux-system ns
@@ -0,0 +1,25 @@
# This manifest is FOR REFERENCE — the live version must be committed to
# 0xWheatyz/Talos at testing1/first-cluster/cluster/flux/api-company-source/
#
# See leeworks-agents/api-company#2
# See leeworks-agents/api-company#97
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: api-company
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: api-company
path: ./flux
prune: true
wait: true
timeout: 5m
postBuild:
substituteFrom:
- kind: Secret
name: grafana-admin
optional: false
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: bedag
namespace: flux-system
spec:
interval: 60m
url: https://bedag.github.io/helm-charts/
+89
View File
@@ -0,0 +1,89 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: docs-site
namespace: docs-site
spec:
interval: 10m
chart:
spec:
chart: raw
version: ">=0.2.0"
sourceRef:
kind: HelmRepository
name: bedag
namespace: flux-system
interval: 60m
values:
resources:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: docs-site
namespace: docs-site
spec:
replicas: 1
selector:
matchLabels:
app: docs-site
template:
metadata:
labels:
app: docs-site
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: docs-site
image: registry.leeworks.dev/leeworks-agents/docs-site:latest # {"$imagepolicy": "flux-system:docs-site"}
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 30
- apiVersion: v1
kind: Service
metadata:
name: docs-site
namespace: docs-site
spec:
selector:
app: docs-site
ports:
- port: 80
targetPort: 80
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: docs-site
namespace: docs-site
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- docs.leeworks.dev
secretName: docs-site-tls
rules:
- host: docs.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docs-site
port:
number: 80
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrelease.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: docs-site
@@ -0,0 +1,50 @@
# ClusterSecretStore using the Kubernetes provider.
# Reads secrets from the cluster itself — no external vault required.
# The service account below must have get/list access to secrets in
# the namespaces where ExternalSecrets are created.
apiVersion: v1
kind: ServiceAccount
metadata:
name: eso-kubernetes-provider
namespace: external-secrets
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: eso-kubernetes-provider-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: eso-kubernetes-provider-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: eso-kubernetes-provider-reader
subjects:
- kind: ServiceAccount
name: eso-kubernetes-provider
namespace: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: kubernetes-provider
spec:
provider:
kubernetes:
remoteNamespace: external-secrets
server:
caProvider:
type: ConfigMap
name: kube-root-ca.crt
namespace: external-secrets
key: ca.crt
auth:
serviceAccount:
name: eso-kubernetes-provider
namespace: external-secrets
+30
View File
@@ -0,0 +1,30 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets
namespace: external-secrets
spec:
interval: 15m
chart:
spec:
chart: external-secrets
version: ">=0.9.0 <1.0.0"
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
interval: 60m
install:
crds: CreateReplace
remediation:
retries: 3
upgrade:
crds: CreateReplace
remediation:
retries: 3
values:
installCRDs: true
webhook:
port: 9443
certController:
requeueInterval: 5m
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: external-secrets
namespace: flux-system
spec:
interval: 60m
url: https://charts.external-secrets.io
+7
View File
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
- clustersecretstore.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: external-secrets
+42
View File
@@ -0,0 +1,42 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: gitea-act-runner
namespace: gitea-runner
spec:
interval: 10m
chart:
spec:
chart: gitea-act-runner
version: ">=0.1.0"
sourceRef:
kind: HelmRepository
name: gitea-charts
namespace: flux-system
interval: 60m
values:
replicaCount: 1
config:
registration:
# Gitea instance URL
instanceUrl: "https://gitea.leeworks.dev"
# Token from Gitea admin → Actions → Runners → New Runner
# Store in a Kubernetes Secret named gitea-runner-token
tokenFromSecret:
secretName: gitea-runner-token
secretKey: token
runner:
# Register at org scope so all leeworks-agents repos can use it
labels:
- "ubuntu-latest:docker://node:20-bookworm"
- "ubuntu-22.04:docker://node:20-bookworm"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 2000m
memory: 2Gi
# Runner needs Docker socket or dind
dind:
enabled: true
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: gitea-charts
namespace: flux-system
spec:
interval: 60m
url: https://dl.gitea.com/charts/
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: gitea-runner
+18
View File
@@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rapidapi-proxy-secret
namespace: holidays
spec:
refreshInterval: 1h
secretStoreRef:
name: kubernetes-provider
kind: ClusterSecretStore
target:
name: rapidapi-proxy-secret
creationPolicy: Owner
data:
- secretKey: X-RapidAPI-Proxy-Secret
remoteRef:
key: rapidapi-holidays-source
property: X-RapidAPI-Proxy-Secret
+101
View File
@@ -0,0 +1,101 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: holidays
namespace: holidays
spec:
interval: 10m
chart:
spec:
chart: raw
version: ">=0.2.0"
sourceRef:
kind: HelmRepository
name: bedag
namespace: flux-system
interval: 60m
values:
resources:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: holidays
namespace: holidays
spec:
replicas: 1
selector:
matchLabels:
app: holidays
template:
metadata:
labels:
app: holidays
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: holidays
image: registry.leeworks.dev/holidays/server:latest # {"$imagepolicy": "flux-system:holidays"}
ports:
- containerPort: 3000
env:
- name: RAPIDAPI_PROXY_SECRET
valueFrom:
secretKeyRef:
name: rapidapi-proxy-secret
key: X-RapidAPI-Proxy-Secret
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
- apiVersion: v1
kind: Service
metadata:
name: holidays
namespace: holidays
spec:
selector:
app: holidays
ports:
- port: 80
targetPort: 3000
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: holidays
namespace: holidays
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- holidays.leeworks.dev
secretName: holidays-tls
rules:
- host: holidays.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: holidays
port:
number: 80
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- externalsecret.yaml
- helmrelease.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: holidays
+51
View File
@@ -0,0 +1,51 @@
# ImagePolicy: select the latest semver tag from each ImageRepository.
# Tags pushed by CI should follow semver (e.g. v1.2.3) or use "latest" —
# the semver policy picks up any vX.Y.Z tag. The "latest" alias keeps
# things working before formal releases are tagged.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: zip-enrichment
namespace: flux-system
spec:
imageRepositoryRef:
name: zip-enrichment
policy:
semver:
range: ">=0.1.0"
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: holidays
namespace: flux-system
spec:
imageRepositoryRef:
name: holidays
policy:
semver:
range: ">=0.1.0"
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: air-quality
namespace: flux-system
spec:
imageRepositoryRef:
name: air-quality
policy:
semver:
range: ">=0.1.0"
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: docs-site
namespace: flux-system
spec:
imageRepositoryRef:
name: docs-site
policy:
alphabetical:
order: asc
@@ -0,0 +1,45 @@
# Flux image-reflector-controller watches these registries for new image tags.
# Requires: flux-system/gitea-image-automation-token secret (see docs/operator-runbook.md)
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: zip-enrichment
namespace: flux-system
spec:
image: registry.leeworks.dev/zip-enrichment/server
interval: 5m
secretRef:
name: gitea-leeworks-agents-token
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: holidays
namespace: flux-system
spec:
image: registry.leeworks.dev/holidays/server
interval: 5m
secretRef:
name: gitea-leeworks-agents-token
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: air-quality
namespace: flux-system
spec:
image: registry.leeworks.dev/air-quality/server
interval: 5m
secretRef:
name: gitea-leeworks-agents-token
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: docs-site
namespace: flux-system
spec:
image: registry.leeworks.dev/leeworks-agents/docs-site
interval: 5m
secretRef:
name: gitea-leeworks-agents-token
@@ -0,0 +1,39 @@
# ImageUpdateAutomation: when an ImagePolicy selects a new tag, this object
# instructs Flux to open a commit on the api-company repo updating the
# image reference in the relevant HelmRelease values.
#
# The GitRepository used here is the api-company source (flux-system/api-company).
# Flux needs write access; create the token secret first:
# kubectl create secret generic gitea-image-automation-token \
# -n flux-system \
# --from-literal=username=leeworks-agents \
# --from-literal=password=<TOKEN_WITH_WRITE_REPO>
# Then patch the api-company GitRepository to reference it (or reuse
# gitea-leeworks-agents-token if that token also has write:repository scope).
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
name: api-company
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: api-company
git:
checkout:
ref:
branch: main
commit:
author:
email: agent@leeworks.dev
name: Flux Image Automation
messageTemplate: |
chore(image): update {{range .Updated.Images}}{{.Repository}}:{{.NewTag}} {{end}}
push:
branch: main
secretRef:
name: gitea-image-automation-token # must pre-exist in flux-system ns — see docs/secrets-checklist.md item 9
update:
path: ./flux
strategy: Setters
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- imagerepositories.yaml
- imagepolicies.yaml
- imageupdateautomation.yaml
+12
View File
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- bedag-helmrepository.yaml
- external-secrets
- gitea-runner
- monitoring
- docs-site
- zip-enrichment
- holidays
- air-quality
- image-automation
+94
View File
@@ -0,0 +1,94 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: gatus
namespace: monitoring
spec:
interval: 15m
chart:
spec:
chart: gatus
version: ">=1.0.0"
sourceRef:
kind: HelmRepository
name: minicloudlabs
namespace: flux-system
interval: 60m
values:
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: status.leeworks.dev
paths:
- path: /
pathType: Prefix
tls:
- secretName: gatus-tls
hosts:
- status.leeworks.dev
config:
storage:
type: sqlite
path: /data/gatus.db
endpoints:
- name: ZIP Enrichment API
url: https://zip.leeworks.dev/health
interval: 1m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 1000"
alerts:
- type: slack
description: "ZIP Enrichment API is down"
send-on-resolved: true
- name: Holidays API
url: https://holidays.leeworks.dev/health
interval: 1m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 1000"
alerts:
- type: slack
description: "Holidays API is down"
send-on-resolved: true
- name: Air Quality API
url: https://aqi.leeworks.dev/health
interval: 1m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 1000"
alerts:
- type: slack
description: "Air Quality API is down"
send-on-resolved: true
- name: Docs Site
url: https://docs.leeworks.dev
interval: 5m
conditions:
- "[STATUS] == 200"
- name: Container Registry
url: https://registry.leeworks.dev/v2/
interval: 5m
conditions:
- "[STATUS] == 200"
ui:
title: "leeworks.dev API Status"
description: "Real-time status for all leeworks.dev APIs"
logo: ""
# Retention: 90 days
retention:
days: 90
persistence:
enabled: true
size: 1Gi
mountPath: /data
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: minicloudlabs
namespace: flux-system
spec:
interval: 60m
url: https://minicloudlabs.github.io/helm-charts
+190
View File
@@ -0,0 +1,190 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-apis
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
api-dashboard.json: |
{
"annotations": { "list": [] },
"description": "Request rate, latency, error rate, and data freshness for zip-enrichment, holidays, and air-quality APIs",
"editable": true,
"graphTooltip": 1,
"panels": [
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 1,
"title": "Request Rate",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisLabel": "requests/sec", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 },
"id": 2,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"targets": [
{
"expr": "sum by (api, route) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\"}[5m]))",
"legendFormat": "{{api}} {{route}}",
"refId": "A"
}
],
"title": "Request Rate by API / Route",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisLabel": "error fraction", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.05 },
{ "color": "red", "value": 0.20 }
]
},
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 },
"id": 3,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"targets": [
{
"expr": "sum by (api) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\",status=~\"5..\"}[5m])) / sum by (api) (rate(api_requests_total{api=~\"zip-enrichment|holidays|air-quality\"}[5m]))",
"legendFormat": "{{api}} 5xx error rate",
"refId": "A"
}
],
"title": "5xx Error Rate by API",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 },
"id": 4,
"title": "Latency P50 / P95 / P99",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisLabel": "seconds", "drawStyle": "line", "fillOpacity": 10, "lineWidth": 1, "showPoints": "never" },
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 1.0 },
{ "color": "red", "value": 2.0 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 10 },
"id": 5,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" },
"tooltip": { "mode": "multi" }
},
"targets": [
{
"expr": "histogram_quantile(0.50, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
"legendFormat": "P50 {{api}}",
"refId": "A"
},
{
"expr": "histogram_quantile(0.95, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
"legendFormat": "P95 {{api}}",
"refId": "B"
},
{
"expr": "histogram_quantile(0.99, sum by (api, le) (rate(api_response_duration_seconds_bucket{api=~\"zip-enrichment|holidays|air-quality\"}[5m])))",
"legendFormat": "P99 {{api}}",
"refId": "C"
}
],
"title": "Response Latency P50 / P95 / P99 by API",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 18 },
"id": 6,
"title": "Data Freshness",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"max": 2592000,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 1296000 },
{ "color": "red", "value": 2592000 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 19 },
"id": 7,
"options": {
"orientation": "horizontal",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"targets": [
{
"expr": "api_data_freshness_seconds{api=~\"zip-enrichment|holidays|air-quality\"}",
"legendFormat": "{{api}} ({{dataset}})",
"refId": "A"
}
],
"title": "Data Freshness — alert threshold at 30 days (2592000 s)",
"type": "gauge"
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["api-company", "leeworks"],
"templating": { "list": [] },
"time": { "from": "now-3h", "to": "now" },
"timepicker": {},
"timezone": "browser",
"title": "leeworks.dev API Metrics",
"uid": "leeworks-api-metrics",
"version": 1
}
+92
View File
@@ -0,0 +1,92 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kube-prometheus-stack
namespace: monitoring
spec:
interval: 15m
chart:
spec:
chart: kube-prometheus-stack
version: ">=58.0.0 <60.0.0"
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: flux-system
interval: 60m
install:
crds: CreateReplace
remediation:
retries: 3
upgrade:
crds: CreateReplace
remediation:
retries: 3
values:
grafana:
enabled: true
adminPassword: "${GRAFANA_ADMIN_PASSWORD}" # inject via Secret/substitution
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- grafana.leeworks.dev
tls:
- secretName: grafana-tls
hosts:
- grafana.leeworks.dev
persistence:
enabled: true
size: 5Gi
sidecar:
dashboards:
enabled: true
prometheus:
prometheusSpec:
retention: 30d
storageSpec:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 20Gi
# Scrape pods with prometheus.io/scrape=true annotations
podMonitorNamespaceSelector: {}
podMonitorSelector: {}
serviceMonitorNamespaceSelector: {}
serviceMonitorSelector: {}
# Additional scrape configs for annotation-based discovery
additionalScrapeConfigs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name
alertmanager:
enabled: false # Enable when alert routing is configured
kubeStateMetrics:
enabled: true
nodeExporter:
enabled: true
+8
View File
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: prometheus-community
namespace: flux-system
spec:
interval: 60m
url: https://prometheus-community.github.io/helm-charts
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- helmrepository.yaml
- helmrelease.yaml
- gatus-helmrepository.yaml
- gatus-helmrelease.yaml
- grafana-dashboard-apis.yaml
- prometheusrule-apis.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: monitoring
+110
View File
@@ -0,0 +1,110 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: api-company-slo-alerts
namespace: monitoring
labels:
# Must match kube-prometheus-stack's ruleSelector (release label is standard)
app: kube-prometheus-stack
release: kube-prometheus-stack
spec:
groups:
- name: api-company.slo
interval: 1m
rules:
# -------------------------------------------------------------------
# APIHighErrorRate — warning: >5% 5xx over 5 min
# -------------------------------------------------------------------
- alert: APIHighErrorRate
expr: |
(
sum by (job) (rate(api_requests_total{status=~"5..", job=~"zip|holidays|air-quality"}[5m]))
/
sum by (job) (rate(api_requests_total{job=~"zip|holidays|air-quality"}[5m]))
) > 0.05
for: 5m
labels:
severity: warning
team: api-company
annotations:
summary: "High 5xx error rate on {{ $labels.job }}"
description: "{{ $labels.job }} 5xx error rate is {{ $value | humanizePercentage }} over the last 5 minutes (threshold: 5%)."
# -------------------------------------------------------------------
# APIHighErrorRate — critical: >20% 5xx over 5 min
# -------------------------------------------------------------------
- alert: APIHighErrorRate
expr: |
(
sum by (job) (rate(api_requests_total{status=~"5..", job=~"zip|holidays|air-quality"}[5m]))
/
sum by (job) (rate(api_requests_total{job=~"zip|holidays|air-quality"}[5m]))
) > 0.20
for: 5m
labels:
severity: critical
team: api-company
annotations:
summary: "Critical 5xx error rate on {{ $labels.job }}"
description: "{{ $labels.job }} 5xx error rate is {{ $value | humanizePercentage }} over the last 5 minutes (threshold: 20%)."
# -------------------------------------------------------------------
# APIHighLatency — P95 > 2 s over 5 min
# -------------------------------------------------------------------
- alert: APIHighLatency
expr: |
histogram_quantile(
0.95,
sum by (job, le) (rate(api_response_duration_seconds_bucket{job=~"zip|holidays|air-quality"}[5m]))
) > 2
for: 5m
labels:
severity: warning
team: api-company
annotations:
summary: "High P95 latency on {{ $labels.job }}"
description: "{{ $labels.job }} P95 response time is {{ $value | humanizeDuration }} (threshold: 2s)."
# -------------------------------------------------------------------
# APIDataStale — data freshness > 30 days
# -------------------------------------------------------------------
- alert: APIDataStale
expr: |
api_data_freshness_seconds{job=~"zip|holidays|air-quality"} > 2592000
for: 30m
labels:
severity: warning
team: api-company
annotations:
summary: "Stale dataset on {{ $labels.job }} ({{ $labels.dataset }})"
description: "{{ $labels.job }} dataset '{{ $labels.dataset }}' has not been re-seeded in {{ $value | humanizeDuration }} (threshold: 30 days). Re-seed required."
# -------------------------------------------------------------------
# APIDataCriticallyStale — data freshness > 60 days
# -------------------------------------------------------------------
- alert: APIDataCriticallyStale
expr: |
api_data_freshness_seconds{job=~"zip|holidays|air-quality"} > 5184000
for: 1h
labels:
severity: critical
team: api-company
annotations:
summary: "API data is critically stale on {{ $labels.job }}"
description: "{{ $labels.job }} data has not been re-seeded in more than 60 days ({{ $value | humanizeDuration }})"
# -------------------------------------------------------------------
# APIDown — any API job absent for 2 min
# -------------------------------------------------------------------
- alert: APIDown
expr: |
absent(up{job=~"zip|holidays|air-quality"} == 1)
or
up{job=~"zip|holidays|air-quality"} == 0
for: 2m
labels:
severity: critical
team: api-company
annotations:
summary: "API service {{ $labels.job }} is down"
description: "Prometheus target {{ $labels.job }} has been unreachable for more than 2 minutes."
+18
View File
@@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rapidapi-proxy-secret
namespace: zip-enrichment
spec:
refreshInterval: 1h
secretStoreRef:
name: kubernetes-provider
kind: ClusterSecretStore
target:
name: rapidapi-proxy-secret
creationPolicy: Owner
data:
- secretKey: X-RapidAPI-Proxy-Secret
remoteRef:
key: rapidapi-zip-enrichment-source
property: X-RapidAPI-Proxy-Secret
+101
View File
@@ -0,0 +1,101 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: zip-enrichment
namespace: zip-enrichment
spec:
interval: 10m
chart:
spec:
chart: raw
version: ">=0.2.0"
sourceRef:
kind: HelmRepository
name: bedag
namespace: flux-system
interval: 60m
values:
resources:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: zip-enrichment
namespace: zip-enrichment
spec:
replicas: 1
selector:
matchLabels:
app: zip-enrichment
template:
metadata:
labels:
app: zip-enrichment
spec:
imagePullSecrets:
- name: gitea-registry
containers:
- name: zip-enrichment
image: registry.leeworks.dev/zip-enrichment/server:latest # {"$imagepolicy": "flux-system:zip-enrichment"}
ports:
- containerPort: 3000
env:
- name: RAPIDAPI_PROXY_SECRET
valueFrom:
secretKeyRef:
name: rapidapi-proxy-secret
key: X-RapidAPI-Proxy-Secret
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
- apiVersion: v1
kind: Service
metadata:
name: zip-enrichment
namespace: zip-enrichment
spec:
selector:
app: zip-enrichment
ports:
- port: 80
targetPort: 3000
- apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zip-enrichment
namespace: zip-enrichment
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- zip.leeworks.dev
secretName: zip-enrichment-tls
rules:
- host: zip.leeworks.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: zip-enrichment
port:
number: 80
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- externalsecret.yaml
- helmrelease.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: zip-enrichment
-1
View File
@@ -1 +0,0 @@
# placeholder — populated by Phase-4/5 issues
+41
View File
@@ -27,3 +27,44 @@ Building next: {name} because {reason}
---
_(No sessions yet — first research run will be triggered by `/sprint` when phase-1 of all 3 initial APIs has issues filed.)_
## Research Session 2026-05-29 15:30
### Searches run
1. RapidAPI most subscribed APIs 2026
2. Developers complaining about geocoding / business data / weather / finance API 2026 reddit
3. Free public dataset API niche no existing wrapper
4. site:rapidapi.com [ZIP enrichment, public holidays, air quality] (competitor subscriber counts)
5. "is there an API for" site:reddit.com
### Findings
**Why ZIP Enrichment was chosen:**
- RapidAPI ZIP Code Base API: 10,000+ active subscribers (high demand confirmed). Competitors charge $29+/mo for similar data. Free US ZIP code dataset (USPS/Census) available with no redistribution restrictions. Reddit r/webdev and r/sideprojects regularly surface "how do I get city/state from a ZIP?" questions.
- Competitor: `zipcodeapi.com` has no RapidAPI listing; `zippopotam.us` (free, no paid tier) leaves a clear paid-tier gap. [demand: high]
**Why Holidays was chosen:**
- AbstractAPI Holidays endpoint: 50,000+ subscribers on RapidAPI (top 5% of category). holidayapi.com charges $19/mo for >1 country/year. Nager.Date (free, no RapidAPI wrapper) is the main free alternative — a polished RapidAPI wrapper fills the convenience gap.
- Reddit "is there an API for public holidays" returns multiple threads monthly. [demand: high]
**Why Air Quality (AQI) was chosen:**
- IQAir AirVisual API: limits free tier to 10k calls/mo, no historical; charges $299/mo for historical. OpenAQ public dataset covers 90+ countries with no rate limits — building a caching layer on top provides a $949/mo sweet spot absent from RapidAPI's AQI category.
- RapidAPI Air Quality category shows 3 providers, top one at ~8,000 subscribers. [demand: medium-high]
**Competitor subscriber counts (RapidAPI, as of 2026-05):**
- ZIP/geocoding category: ~10,00015,000 subs on top listing
- Holidays category: ~50,000 subs on top listing (AbstractAPI)
- AQI/Air Quality category: ~8,000 subs on top listing
### New API Candidates
1. **Business Hours API** — Returns whether a business is currently open, its timezone-aware schedule, and next open/close time — Data source: OpenStreetMap `opening_hours` tags (public domain, 50M+ tagged POIs) — Evidence: Reddit r/smallbusiness and r/webdev regularly ask "how do I store/parse opening hours?"; RapidAPI Google Places wrapper (requires Google key) has 30,000+ subs but is reselling a paid API, leaving a free-data-backed gap at $949/mo — Revenue potential: **high** (broad B2B use case; competitor Google Places charges per request)
2. **Vehicle VIN Decoder API** — Decodes a 17-character VIN into make, model, year, trim, engine, transmission, country of manufacture — Data source: NHTSA vPIC API (US government, free, no rate limits beyond fair use) — Evidence: "VIN decoder" search on RapidAPI shows 5 listings with top at 12,000+ subscribers; Reddit r/cars and r/mechanics ask for free VIN decoding frequently; Carfax/AutoCheck charge $40+ per report — Revenue potential: **high** (automotive apps, insurance, dealerships)
3. **Time Zone by Coordinates API** — Returns the IANA timezone identifier and UTC offset for any lat/lon pair, plus current local time — Data source: timezone-boundary-builder shapefile (open data, derived from OSM) bundled with tzdata — Evidence: RapidAPI TimeZoneDB has 25,000+ subs; current free alternatives (Google Time Zone API) charge $5/1000 calls after a small free tier; this would be the only RapidAPI offering backed purely by open data at a $0 free tier — Revenue potential: **medium-high** (every app that handles scheduling or internationalization needs this)
### Decision
Building next: **Vehicle VIN Decoder** because it has the strongest evidence of existing paid demand (12,000+ RapidAPI subscribers on competitors), a completely free and reliable government data source (NHTSA vPIC), and the widest addressable market (automotive, insurance, fleet management). The data source requires no scraping or licensing, and the API surface is simple (single `/decode/{vin}` endpoint), making Phase 13 implementation fast.
Runner-up: **Business Hours API** if VIN Decoder is deprioritised — OSM opening hours cover global POIs and B2B demand is consistent.
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* publish-openapi.js
*
* Uploads each API's openapi.yaml to the RapidAPI Platform API.
* Environment variables required per API (skips silently if not set):
* RAPIDAPI_KEY — RapidAPI Platform API bearer token
* RAPIDAPI_ZIP_API_ID — API ID for ZIP Enrichment on RapidAPI
* RAPIDAPI_ZIP_VERSION_ID — Version ID for ZIP Enrichment
* RAPIDAPI_HOLIDAYS_API_ID
* RAPIDAPI_HOLIDAYS_VERSION_ID
* RAPIDAPI_AQI_API_ID
* RAPIDAPI_AQI_VERSION_ID
*
* Usage: node scripts/publish-openapi.js
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || process.env.RAPIDAPI_PLATFORM_KEY;
if (!RAPIDAPI_KEY) {
console.log('⚠️ RAPIDAPI_KEY / RAPIDAPI_PLATFORM_KEY not set — skipping all spec uploads.');
process.exit(0);
}
const APIS = [
{
name: 'zip-enrichment',
specPath: path.join(__dirname, '..', 'apis', 'zip-enrichment', 'openapi.yaml'),
apiId: process.env.RAPIDAPI_ZIP_API_ID,
versionId: process.env.RAPIDAPI_ZIP_VERSION_ID,
},
{
name: 'holidays',
specPath: path.join(__dirname, '..', 'apis', 'holidays', 'openapi.yaml'),
apiId: process.env.RAPIDAPI_HOLIDAYS_API_ID,
versionId: process.env.RAPIDAPI_HOLIDAYS_VERSION_ID,
},
{
name: 'air-quality',
specPath: path.join(__dirname, '..', 'apis', 'air-quality', 'openapi.yaml'),
apiId: process.env.RAPIDAPI_AQI_API_ID,
versionId: process.env.RAPIDAPI_AQI_VERSION_ID,
},
];
/**
* Build a multipart/form-data body from a file buffer.
* Returns { body: Buffer, boundary: string }
*/
function buildMultipart(fieldName, filename, fileBuffer, contentType = 'application/yaml') {
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);
const CRLF = '\r\n';
const parts = [
Buffer.from(
`--${boundary}${CRLF}` +
`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"${CRLF}` +
`Content-Type: ${contentType}${CRLF}${CRLF}`
),
fileBuffer,
Buffer.from(`${CRLF}--${boundary}--${CRLF}`),
];
return { body: Buffer.concat(parts), boundary };
}
/**
* Upload a spec file to RapidAPI Platform API.
* Returns a promise that resolves with the response status code.
*/
function uploadSpec(api) {
return new Promise((resolve, reject) => {
if (!api.apiId || !api.versionId) {
console.log(`⏭️ Skipping ${api.name}: API ID or Version ID not configured.`);
return resolve(null);
}
if (!fs.existsSync(api.specPath)) {
console.log(`⏭️ Skipping ${api.name}: spec file not found at ${api.specPath}`);
return resolve(null);
}
const fileBuffer = fs.readFileSync(api.specPath);
const { body, boundary } = buildMultipart('spec', 'openapi.yaml', fileBuffer);
const options = {
hostname: 'platformapi1.p.rapidapi.com',
path: `/v1/apis/${api.apiId}/versions/${api.versionId}`,
method: 'PUT',
headers: {
'Authorization': `Bearer ${RAPIDAPI_KEY}`,
'X-RapidAPI-Key': RAPIDAPI_KEY,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length,
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log(`✓ Published ${api.name} spec to RapidAPI (HTTP ${res.statusCode})`);
resolve(res.statusCode);
} else {
reject(new Error(`Failed to publish ${api.name}: HTTP ${res.statusCode}${data}`));
}
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
async function main() {
let hasError = false;
for (const api of APIS) {
try {
await uploadSpec(api);
} catch (err) {
console.error(`${err.message}`);
hasError = true;
}
}
if (hasError) {
process.exit(1);
}
}
main();