---
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));
```

<div style="display:flex; gap:1.5rem; flex-wrap:wrap; margin-bottom:2rem;">
  <div style="background:#fef2f2; border:1px solid #fca5a5; border-radius:8px; padding:1rem 1.5rem; min-width:160px;">
    <div style="font-size:2rem; font-weight:700; color:#dc2626;">${gdprReport.warning_count}</div>
    <div style="font-size:0.85rem; color:#6b7280;">GDPR warnings</div>
  </div>
  <div style="background:#f0fdf4; border:1px solid #86efac; border-radius:8px; padding:1rem 1.5rem; min-width:160px;">
    <div style="font-size:2rem; font-weight:700; color:#16a34a;">${gdprReport.total_services}</div>
    <div style="font-size:0.85rem; color:#6b7280;">Services in catalog</div>
  </div>
  <div style="background:#fffbeb; border:1px solid #fcd34d; border-radius:8px; padding:1rem 1.5rem; min-width:160px;">
    <div style="font-size:2rem; font-weight:700; color:#b45309;">${paidServices.length}</div>
    <div style="font-size:0.85rem; color:#6b7280;">Paid / usage-based</div>
  </div>
</div>

---

## Service Catalog

```js
import {html} from "npm:htl";

function maturityBadge(m) {
  const color = maturityColor[m] || "#9ca3af";
  const label = maturityLabel[m] || m;
  return html`<span style="background:${color}20; color:${color}; border:1px solid ${color}60;
    border-radius:4px; padding:2px 8px; font-size:0.78rem; font-weight:600; white-space:nowrap;">${label}</span>`;
}

function pricingBadge(p) {
  const colors = { paid: "#7c3aed", usage_based: "#7c3aed", freemium: "#0369a1", free: "#166534", unknown: "#6b7280" };
  const c = colors[p] || "#6b7280";
  return html`<span style="color:${c}; font-size:0.78rem; font-weight:500;">${p.replace("_", " ")}</span>`;
}

const catalogTable = html`<table style="width:100%; border-collapse:collapse; font-size:0.9rem;">
  <thead>
    <tr style="border-bottom:2px solid #e5e7eb;">
      <th style="text-align:left; padding:8px 12px;">Service</th>
      <th style="text-align:left; padding:8px 12px;">Provider</th>
      <th style="text-align:left; padding:8px 12px;">Category</th>
      <th style="text-align:left; padding:8px 12px;">Pricing</th>
      <th style="text-align:left; padding:8px 12px;">GDPR Maturity</th>
      <th style="text-align:left; padding:8px 12px;">DPA</th>
    </tr>
  </thead>
  <tbody>
    ${catalog.map(s => html`<tr style="border-bottom:1px solid #f3f4f6; ${WARNING_LEVELS.has(s.gdpr_maturity) ? 'background:#fff7f7;' : ''}">
      <td style="padding:8px 12px; font-weight:500;">
        ${s.website_url
          ? html`<a href="${s.website_url}" target="_blank" style="color:#1d4ed8;">${s.name}</a>`
          : s.name}
      </td>
      <td style="padding:8px 12px; color:#6b7280;">${s.provider || "—"}</td>
      <td style="padding:8px 12px; color:#6b7280;">${s.category || "—"}</td>
      <td style="padding:8px 12px;">${pricingBadge(s.pricing_model)}</td>
      <td style="padding:8px 12px;">${maturityBadge(s.gdpr_maturity)}</td>
      <td style="padding:8px 12px;">${s.dpa_available ? "✅" : "❌"}</td>
    </tr>`)}
  </tbody>
</table>`;
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`<p style="color:#16a34a;">✅ No GDPR warnings across active repos.</p>`);
} else {
  const warningCards = html`<div style="display:flex; flex-direction:column; gap:0.75rem;">
    ${gdprReport.warnings.map(w => {
      const color = maturityColor[w.gdpr_maturity] || "#ef4444";
      return html`<div style="border-left:4px solid ${color}; background:${color}10; padding:0.75rem 1rem; border-radius:4px;">
        <div style="font-weight:600;">${w.service_slug}
          <span style="font-weight:400; color:#6b7280; margin-left:0.5rem;">in ${w.repo_slug || "unknown repo"}</span>
        </div>
        <div style="font-size:0.85rem; margin-top:2px;">
          ${maturityBadge(w.gdpr_maturity)}
          ${w.pricing_model ? html`&nbsp;${pricingBadge(w.pricing_model)}` : ""}
          ${w.purpose ? html`<span style="color:#6b7280; margin-left:0.5rem;">— ${w.purpose}</span>` : ""}
        </div>
      </div>`;
    })}
  </div>`;
  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`<div class="repo-ref">
    <div class="repo-ref-name">
      ${webUrl
        ? html`<a href="${webUrl}" target="_blank" rel="noopener noreferrer">${label}</a>`
        : label}
    </div>
    <div class="repo-ref-meta">${domain} / ${slug}</div>
  </div>`;
  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`<table style="width:100%; border-collapse:collapse; font-size:0.9rem;">
  <thead>
    <tr style="border-bottom:2px solid #e5e7eb;">
      <th style="text-align:left; padding:8px 12px;">Repo</th>
      <th style="text-align:left; padding:8px 12px;">Services</th>
      <th style="text-align:left; padding:8px 12px;">Ingested</th>
    </tr>
  </thead>
  <tbody>
    ${[...repoBreakdown.entries()].map(([repoKey, snap]) => html`<tr style="border-bottom:1px solid #f3f4f6;">
      <td style="padding:8px 12px; font-weight:500;">${repoCell(repoKey)}</td>
      <td style="padding:8px 12px;">
        ${snap.entries.map(e => {
          const cat = catalogBySlug[e.service_slug];
          const m = cat?.gdpr_maturity || "unknown";
          const color = maturityColor[m] || "#9ca3af";
          return html`<span style="display:inline-flex; align-items:center; gap:4px; margin:2px 4px 2px 0;
            background:${color}15; border:1px solid ${color}50; border-radius:4px; padding:2px 8px; font-size:0.8rem;">
            <span style="width:8px; height:8px; border-radius:50%; background:${color}; display:inline-block;"></span>
            ${e.service_slug}
          </span>`;
        })}
      </td>
      <td style="padding:8px 12px; color:#9ca3af; font-size:0.8rem;">${new Date(snap.snapshot_at).toLocaleDateString()}</td>
    </tr>`)}
  </tbody>
</table>`;
display(repoTable);
```

<style>
.repo-ref {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.repo-ref-name {
  font-weight: 600;
  color: var(--theme-foreground, #111);
}
.repo-ref-name a {
  color: #1d4ed8;
  text-decoration: none;
}
.repo-ref-name a:hover {
  text-decoration: underline;
}
.repo-ref-meta {
  font-size: 0.76rem;
  font-weight: 400;
  color: var(--theme-foreground-muted, #6b7280);
}
</style>
