--- 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` ${catalog.map(s => html``)}
Service Provider Category Pricing GDPR Maturity DPA
${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`
${webUrl ? html`${label}` : label}
${domain} / ${slug}
`; 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` ${[...repoBreakdown.entries()].map(([repoKey, snap]) => html``)}
Repo Services Ingested
${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); ```