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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,5 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Placeholder
run: |
node --version
echo "Add linting and tests with the first implementation PR."
- name: Validate static dashboard
run: node scripts/validate.mjs
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,17 @@ The dashboard must only consume aggregate, publication-approved data. No raw con
## First Milestone

Build a static dashboard using synthetic aggregate data, including suppression rules for cohorts below the publication threshold.

## Current Prototype

This repo includes a static synthetic aggregate dashboard:

```bash
python3 -m http.server 8000
```

Then open `http://127.0.0.1:8000`.

The dashboard reads `data/synthetic_aggregate.json`, displays aggregate dimension counts,
and suppresses cohorts below `min_public_cohort`. It must not ship raw conversations,
quotes, individual records, schools, locations, or reviewer-only data.
25 changes: 25 additions & 0 deletions data/synthetic_aggregate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"schema_version": "0.1.0",
"status": "Synthetic draft",
"record_count": 12,
"min_public_cohort": 5,
"generated_at": "2026-07-12",
"dimensions": [
{
"name": "academic_support",
"count": 8
},
{
"name": "emotional_support",
"count": 6
},
{
"name": "identity_or_relationships",
"count": 4
},
{
"name": "safety_sensitive",
"count": 2
}
]
}
50 changes: 50 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Make AI Visible Synthetic Dashboard</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="shell">
<header>
<p class="eyebrow">Synthetic aggregate MVP</p>
<h1>Make AI Visible dashboard</h1>
<p>
Public views should show aggregate, publication-approved patterns only. This
prototype uses synthetic data and suppresses cohorts below the privacy threshold.
</p>
</header>

<section class="summary" aria-label="Dataset summary">
<article>
<span id="record-count">0</span>
<p>synthetic records</p>
</article>
<article>
<span id="threshold">0</span>
<p>minimum cohort threshold</p>
</article>
<article>
<span id="freshness">Draft</span>
<p>data status</p>
</article>
</section>

<section class="panel">
<h2>Behavioral dimensions</h2>
<div id="chart" class="chart"></div>
</section>

<section class="panel">
<h2>Methodology</h2>
<p id="methodology">
No individual conversations, quotes, schools, locations, or small cohorts should
appear in this dashboard.
</p>
</section>
</main>
<script src="src/dashboard.js"></script>
</body>
</html>
18 changes: 18 additions & 0 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { readFile } from "node:fs/promises";

const aggregate = JSON.parse(await readFile("data/synthetic_aggregate.json", "utf8"));

if (aggregate.min_public_cohort < 5) {
throw new Error("min_public_cohort must be at least 5 for the MVP demo");
}

if (aggregate.dimensions.some((dimension) => typeof dimension.count !== "number")) {
throw new Error("all dimensions need numeric counts");
}

const html = await readFile("index.html", "utf8");
if (!html.includes("src/dashboard.js")) {
throw new Error("dashboard script is not linked");
}

console.log("Dashboard prototype validation passed.");
42 changes: 42 additions & 0 deletions src/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
async function loadAggregate() {
const response = await fetch("data/synthetic_aggregate.json");
return response.json();
}

function render(data) {
document.querySelector("#record-count").textContent = data.record_count;
document.querySelector("#threshold").textContent = data.min_public_cohort;
document.querySelector("#freshness").textContent = data.status;

const chart = document.querySelector("#chart");
chart.replaceChildren();

for (const dimension of data.dimensions) {
const row = document.createElement("div");
row.className = "bar-row";

const meta = document.createElement("div");
meta.className = "bar-meta";

const label = document.createElement("strong");
label.textContent = dimension.name.replaceAll("_", " ");

const value = document.createElement("span");
const suppressed = dimension.count < data.min_public_cohort;
value.textContent = suppressed ? "Suppressed" : `${dimension.count} records`;
if (suppressed) value.className = "suppressed";

const track = document.createElement("div");
track.className = "bar-track";
const fill = document.createElement("div");
fill.className = "bar-fill";
fill.style.width = suppressed ? "0%" : `${(dimension.count / data.record_count) * 100}%`;
track.append(fill);

meta.append(label, value);
row.append(meta, track);
chart.append(row);
}
}

loadAggregate().then(render);
134 changes: 134 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
:root {
--ink: #17212f;
--muted: #5a6878;
--line: #d1dbe5;
--surface: #ffffff;
--wash: #f1f5f2;
--accent: #3b755f;
--accent-2: #5f6fa3;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
background: var(--wash);
color: var(--ink);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.shell {
width: min(100%, 1040px);
margin: 0 auto;
padding: 28px 16px 40px;
}

header {
padding: 22px 0;
}

.eyebrow {
margin: 0 0 8px;
color: var(--accent);
font-size: 0.82rem;
font-weight: 800;
text-transform: uppercase;
}

h1,
h2,
p {
margin-top: 0;
}

h1 {
max-width: 760px;
margin-bottom: 12px;
font-size: clamp(2.2rem, 7vw, 4.5rem);
line-height: 0.98;
}

header p,
.panel p {
max-width: 720px;
color: var(--muted);
line-height: 1.55;
}

.summary {
display: grid;
gap: 12px;
margin-bottom: 16px;
}

.summary article,
.panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
}

.summary article {
padding: 18px;
}

.summary span {
display: block;
margin-bottom: 4px;
font-size: 2rem;
font-weight: 850;
}

.summary p {
margin: 0;
color: var(--muted);
}

.panel {
margin-top: 16px;
padding: 18px;
}

.chart {
display: grid;
gap: 14px;
}

.bar-row {
display: grid;
gap: 6px;
}

.bar-meta {
display: flex;
justify-content: space-between;
gap: 10px;
color: var(--muted);
font-size: 0.92rem;
}

.bar-track {
height: 18px;
overflow: hidden;
border-radius: 6px;
background: #e7edf2;
}

.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
}

.suppressed {
color: #7a5500;
font-weight: 700;
}

@media (min-width: 760px) {
.summary {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
Loading