---
title: Third-Party Services (TPSC)
---
# Third-Party Services Catalog
```js
const API = "http://127.0.0.1:8000";
let apiOk = true;
const catalog = await fetch(`${API}/tpsc/catalog/`)
.then(r => r.json())
.catch(() => { apiOk = false; return []; });
const gdprReport = await fetch(`${API}/tpsc/report/gdpr`)
.then(r => r.json())
.catch(() => ({ warnings: [], by_maturity: {}, total_services: 0, warning_count: 0 }));
const snapshots = await fetch(`${API}/tpsc/snapshots/`)
.then(r => r.json())
.catch(() => []);
const repos = await fetch(`${API}/repos/`)
.then(r => r.ok ? r.json() : [])
.catch(() => []);
```
```js
// GDPR maturity colour coding (CNIL/IAPP scale)
const maturityColor = {
unknown: "#ef4444", // red
non_compliant: "#dc2626", // deep red
initial: "#f97316", // orange
developing: "#eab308", // amber
defined: "#84cc16", // lime
managed: "#22c55e", // green
certified: "#16a34a", // deep green
};
const maturityLabel = {
unknown: "Unknown",
non_compliant: "Non-Compliant",
initial: "Initial",
developing: "Developing",
defined: "Defined",
managed: "Managed",
certified: "Certified",
};
const WARNING_LEVELS = new Set(["unknown", "non_compliant", "initial"]);
```
```js
// KPI summary
const warningServices = catalog.filter(s => WARNING_LEVELS.has(s.gdpr_maturity));
const paidServices = catalog.filter(s => ["paid", "usage_based"].includes(s.pricing_model));
```
${gdprReport.warning_count}
GDPR warnings
${gdprReport.total_services}
Services in catalog
${paidServices.length}
Paid / usage-based
---
## Service Catalog
```js
import {html} from "npm:htl";
function maturityBadge(m) {
const color = maturityColor[m] || "#9ca3af";
const label = maturityLabel[m] || m;
return html`${label}`;
}
function pricingBadge(p) {
const colors = { paid: "#7c3aed", usage_based: "#7c3aed", freemium: "#0369a1", free: "#166534", unknown: "#6b7280" };
const c = colors[p] || "#6b7280";
return html`${p.replace("_", " ")}`;
}
const catalogTable = html`
| Service |
Provider |
Category |
Pricing |
GDPR Maturity |
DPA |
${catalog.map(s => html`
|
${s.website_url
? html`${s.name}`
: s.name}
|
${s.provider || "—"} |
${s.category || "—"} |
${pricingBadge(s.pricing_model)} |
${maturityBadge(s.gdpr_maturity)} |
${s.dpa_available ? "✅" : "❌"} |
`)}
`;
display(catalogTable);
```
---
## GDPR Warnings
_Services at **Unknown**, **Non-Compliant**, or **Initial** maturity may limit use in GDPR-regulated or corporate environments._
```js
if (gdprReport.warnings.length === 0) {
display(html`✅ No GDPR warnings across active repos.
`);
} else {
const warningCards = html`
${gdprReport.warnings.map(w => {
const color = maturityColor[w.gdpr_maturity] || "#ef4444";
return html`
${w.service_slug}
in ${w.repo_slug || "unknown repo"}
${maturityBadge(w.gdpr_maturity)}
${w.pricing_model ? html` ${pricingBadge(w.pricing_model)}` : ""}
${w.purpose ? html`— ${w.purpose}` : ""}
`;
})}
`;
display(warningCards);
}
```
---
## Per-Repo Breakdown
```js
const repoById = Object.fromEntries(repos.map(r => [r.id, r]));
const repoBySlug = Object.fromEntries(repos.map(r => [r.slug, r]));
function repoForSnapshotKey(repoKey) {
return repoById[repoKey] ?? repoBySlug[repoKey] ?? null;
}
function repoWebUrl(repo) {
const url = repo?.remote_url ?? "";
return /^https?:\/\//.test(url) ? url : null;
}
function repoCell(repoKey) {
const repo = repoForSnapshotKey(repoKey);
const webUrl = repoWebUrl(repo);
const label = repo?.name || repo?.slug || repoKey || "unknown repo";
const slug = repo?.slug ?? repoKey ?? "unknown";
const domain = repo?.domain_slug ?? "unknown";
const content = html``;
return content;
}
// Build: latest snapshot per repo → service list
const repoBreakdown = new Map();
for (const snap of snapshots) {
const repoKey = snap.repo_id || snap.repo_slug || "unknown";
if (!repoBreakdown.has(repoKey) || snap.snapshot_at > repoBreakdown.get(repoKey).snapshot_at) {
repoBreakdown.set(repoKey, snap);
}
}
// Enrich with catalog data
const catalogBySlug = Object.fromEntries(catalog.map(s => [s.slug, s]));
const repoTable = html`
| Repo |
Services |
Ingested |
${[...repoBreakdown.entries()].map(([repoKey, snap]) => html`
| ${repoCell(repoKey)} |
${snap.entries.map(e => {
const cat = catalogBySlug[e.service_slug];
const m = cat?.gdpr_maturity || "unknown";
const color = maturityColor[m] || "#9ca3af";
return html`
${e.service_slug}
`;
})}
|
${new Date(snap.snapshot_at).toLocaleDateString()} |
`)}
`;
display(repoTable);
```