<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daylight - Privacy Observer</title>
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: #1a1a24;
--bg-hover: #22222e;
--border: #2a2a38;
--border-active: #4a4a62;
--text-primary: #e8e8ef;
--text-secondary: #8b8b9e;
--text-muted: #5e5e72;
--accent: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.15);
--danger: #ef4444;
--danger-glow: rgba(239, 68, 68, 0.15);
--warning: #f59e0b;
--warning-glow: rgba(245, 158, 11, 0.15);
--success: #10b981;
--success-glow: rgba(16, 185, 129, 0.15);
--font: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--mono: 'JetBrains Mono', 'Fira Code', monospace;
--radius: 12px;
--radius-sm: 8px;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 32px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
backdrop-filter: blur(20px);
position: sticky;
top: 0;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent), #8b5cf6);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: white;
}
.brand {
font-size: 20px;
font-weight: 600;
letter-spacing: -0.3px;
}
.brand-sub {
font-size: 12px;
color: var(--text-muted);
font-weight: 400;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-badge.idle {
background: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.status-badge.recording {
background: var(--danger-glow);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
animation: pulse-border 2s ease-in-out infinite;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-badge.recording .status-dot {
animation: pulse-dot 1.5s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
@keyframes pulse-border {
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.2); }
50% { box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); }
}
.main {
display: flex;
height: calc(100vh - 73px);
}
.sidebar {
width: 340px;
border-right: 1px solid var(--border);
background: var(--bg-secondary);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.content {
flex: 1;
overflow-y: auto;
padding: 28px 32px;
}
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.sidebar-tabs {
display: flex;
gap: 4px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
padding: 3px;
}
.sidebar-tab {
flex: 1;
padding: 8px 0;
text-align: center;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.sidebar-tab.active {
background: var(--accent);
color: white;
}
.sidebar-tab:hover:not(.active) {
color: var(--text-primary);
background: var(--bg-hover);
}
.target-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.target-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
}
.target-item:hover {
background: var(--bg-hover);
border-color: var(--border);
}
.target-item.selected {
background: var(--accent-glow);
border-color: var(--accent);
}
.target-icon {
width: 36px;
height: 36px;
background: var(--bg-primary);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.target-info {
flex: 1;
min-width: 0;
}
.target-name {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.target-detail {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.target-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: var(--bg-primary);
color: var(--text-muted);
flex-shrink: 0;
}
.action-bar {
padding: 16px 20px;
border-top: 1px solid var(--border);
}
.btn-record {
width: 100%;
padding: 14px;
border: none;
border-radius: var(--radius-sm);
font-family: var(--font);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-record.start {
background: linear-gradient(135deg, var(--accent), #8b5cf6);
color: white;
}
.btn-record.start:hover {
filter: brightness(1.1);
transform: translateY(-1px);
box-shadow: 0 4px 16px var(--accent-glow);
}
.btn-record.stop {
background: var(--danger);
color: white;
}
.btn-record.stop:hover {
filter: brightness(1.1);
box-shadow: 0 4px 16px var(--danger-glow);
}
.btn-record:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none !important;
filter: none !important;
}
.screen { display: none; }
.screen.active { display: block; }
.welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
padding: 40px;
}
.welcome-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--accent-glow), var(--bg-card));
border: 1px solid var(--border);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
margin-bottom: 24px;
}
.welcome h2 {
font-size: 22px;
font-weight: 600;
margin-bottom: 10px;
}
.welcome p {
font-size: 14px;
color: var(--text-secondary);
max-width: 360px;
line-height: 1.6;
}
.live-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.live-header h2 {
font-size: 18px;
font-weight: 600;
}
.live-stats {
display: flex;
gap: 20px;
}
.live-stat {
text-align: center;
}
.live-stat-value {
font-size: 24px;
font-weight: 700;
font-family: var(--mono);
}
.live-stat-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.live-stat-value.warning { color: var(--warning); }
.live-stat-value.danger { color: var(--danger); }
.live-stat-value.accent { color: var(--accent); }
.flow-section {
margin-top: 28px;
}
.flow-section h3 {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.flow-row {
display: flex;
align-items: center;
padding: 14px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
margin-bottom: 8px;
transition: all 0.2s;
}
.flow-row:hover {
border-color: var(--border-active);
background: var(--bg-hover);
}
.flow-category {
width: 140px;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
}
.flow-arrow {
margin: 0 12px;
color: var(--text-muted);
font-family: var(--mono);
}
.flow-destinations {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.flow-dest {
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-family: var(--mono);
font-weight: 500;
}
.flow-dest.first-party {
background: var(--bg-primary);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.flow-dest.third-party {
background: var(--danger-glow);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.flow-count {
font-size: 11px;
color: var(--text-muted);
margin-left: 8px;
flex-shrink: 0;
font-family: var(--mono);
}
.event-feed {
margin-top: 28px;
}
.event-item {
display: flex;
align-items: center;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
font-family: var(--mono);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.event-time {
color: var(--text-muted);
width: 90px;
flex-shrink: 0;
}
.event-method {
color: var(--accent);
width: 50px;
flex-shrink: 0;
font-weight: 500;
}
.event-domain {
flex: 1;
color: var(--text-primary);
}
.event-domain.third-party {
color: var(--danger);
}
.event-tags {
display: flex;
gap: 4px;
}
.event-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
background: var(--warning-glow);
color: var(--warning);
}
.report-container {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px;
white-space: pre-wrap;
font-family: var(--mono);
font-size: 13px;
line-height: 1.6;
max-height: 70vh;
overflow-y: auto;
}
.report-actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.btn-secondary {
padding: 10px 20px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-card);
color: var(--text-primary);
font-family: var(--font);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
border-color: var(--border-active);
background: var(--bg-hover);
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: var(--text-muted);
font-size: 14px;
gap: 10px;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover { background: var(--border-active); }
</style>
</head>
<body>
<header class="header">
<div class="header-left">
<div class="logo">D</div>
<div>
<div class="brand">Daylight</div>
<div class="brand-sub">Privacy Observer</div>
</div>
</div>
<div id="statusBadge" class="status-badge idle">
<div class="status-dot"></div>
<span id="statusText">Ready</span>
</div>
</header>
<div class="main">
<aside class="sidebar">
<div class="sidebar-header">
<div class="sidebar-tabs">
<button class="sidebar-tab active" data-tab="processes" onclick="switchTab('processes')">Processes</button>
<button class="sidebar-tab" data-tab="browser" onclick="switchTab('browser')">Browser</button>
</div>
</div>
<div id="targetList" class="target-list">
<div class="loading">
<div class="spinner"></div>
Loading...
</div>
</div>
<div class="action-bar">
<button id="btnRecord" class="btn-record start" disabled onclick="toggleRecording()">
<span>▶</span> Start Recording
</button>
</div>
</aside>
<main class="content">
<div id="screenWelcome" class="screen active">
<div class="welcome">
<div class="welcome-icon">☀️</div>
<h2>See what software really sends</h2>
<p>Select a process or browser tab from the sidebar, then click <strong>Start Recording</strong>. Use the app normally. Daylight watches every byte that leaves.</p>
</div>
</div>
<div id="screenLive" class="screen">
<div class="live-header">
<h2 id="liveTitle">Observing...</h2>
<div class="live-stats">
<div class="live-stat">
<div class="live-stat-value accent" id="statEvents">0</div>
<div class="live-stat-label">Events</div>
</div>
<div class="live-stat">
<div class="live-stat-value warning" id="statCategories">0</div>
<div class="live-stat-label">Data Types</div>
</div>
<div class="live-stat">
<div class="live-stat-value danger" id="statDestinations">0</div>
<div class="live-stat-label">Destinations</div>
</div>
</div>
</div>
<div class="flow-section">
<h3>Data Flow</h3>
<div id="flowDiagram"></div>
</div>
<div class="event-feed">
<h3 style="font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin-bottom: 16px;">Live Events</h3>
<div id="eventFeed"></div>
</div>
</div>
<div id="screenReport" class="screen">
<div class="live-header">
<h2>Observation Report</h2>
<div class="report-actions">
<button class="btn-secondary" onclick="copyReport()">📋 Copy</button>
<button class="btn-secondary" onclick="downloadReport()">💾 Save</button>
<button class="btn-secondary" onclick="startNew()">🔄 New</button>
</div>
</div>
<div id="reportContainer" class="report-container"></div>
</div>
</main>
</div>
<script>
let state = {
tab: 'processes',
selectedTarget: null,
recording: false,
events: [],
flows: {},
destinations: new Set(),
categories: new Set(),
report: '',
pollTimer: null,
};
async function api(endpoint, method = 'GET', body = null) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(`/api/${endpoint}`, opts);
return r.json();
}
function switchTab(tab) {
state.tab = tab;
document.querySelectorAll('.sidebar-tab').forEach(t =>
t.classList.toggle('active', t.dataset.tab === tab)
);
state.selectedTarget = null;
updateRecordButton();
loadTargets();
}
async function loadTargets() {
const list = document.getElementById('targetList');
list.innerHTML = '<div class="loading"><div class="spinner"></div>Loading...</div>';
try {
let targets;
if (state.tab === 'processes') {
targets = await api('processes');
list.innerHTML = targets.map((p, i) => `
<div class="target-item" data-idx="${i}" onclick="selectTarget(${i}, 'process', ${p.pid})">
<div class="target-icon">⚙️</div>
<div class="target-info">
<div class="target-name">${esc(p.name)}</div>
<div class="target-detail">PID ${p.pid} · ${esc(p.exe)}</div>
</div>
<span class="target-badge">${formatKiB(p.rss_kib)}</span>
</div>
`).join('');
} else {
targets = await api('browser-tabs');
if (targets.error) {
list.innerHTML = `
<div class="welcome" style="padding: 30px; height: auto;">
<div class="welcome-icon" style="width: 48px; height: 48px; font-size: 20px; margin-bottom: 16px;">🌐</div>
<p style="font-size: 13px; color: var(--text-muted);">
Launch Chrome with:<br>
<code style="font-family: var(--mono); color: var(--accent); font-size: 12px; margin-top: 8px; display: block;">
google-chrome --remote-debugging-port=9222
</code>
</p>
</div>
`;
return;
}
list.innerHTML = targets.map((t, i) => `
<div class="target-item" data-idx="${i}" onclick="selectTarget(${i}, 'browser', ${t.index})">
<div class="target-icon">🌐</div>
<div class="target-info">
<div class="target-name">${esc(t.title)}</div>
<div class="target-detail">${esc(t.url)}</div>
</div>
</div>
`).join('');
}
} catch (e) {
list.innerHTML = `<div class="loading" style="color: var(--danger);">Failed to load targets</div>`;
}
}
function selectTarget(idx, type, id) {
state.selectedTarget = { idx, type, id };
document.querySelectorAll('.target-item').forEach((el, i) =>
el.classList.toggle('selected', i === idx)
);
updateRecordButton();
}
async function toggleRecording() {
if (state.recording) {
await stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
if (!state.selectedTarget) return;
const target = state.selectedTarget;
let body;
if (target.type === 'process') {
body = { type: 'process', pid: target.id };
} else {
body = { type: 'browser', port: 9222, tab_index: target.id };
}
try {
const result = await api('start', 'POST', body);
if (result.error) {
alert('Failed to start: ' + result.error);
return;
}
state.recording = true;
state.events = [];
state.flows = {};
state.destinations = new Set();
state.categories = new Set();
updateRecordButton();
updateStatus('recording');
showScreen('screenLive');
const name = document.querySelector('.target-item.selected .target-name');
document.getElementById('liveTitle').textContent =
`Observing: ${name ? name.textContent : 'target'}`;
state.pollTimer = setInterval(pollEvents, 1000);
} catch (e) {
alert('Connection error: ' + e.message);
}
}
async function stopRecording() {
clearInterval(state.pollTimer);
try {
const result = await api('stop', 'POST');
state.recording = false;
state.report = result.report || '';
updateRecordButton();
updateStatus('idle');
document.getElementById('reportContainer').textContent = state.report;
showScreen('screenReport');
} catch (e) {
state.recording = false;
updateRecordButton();
updateStatus('idle');
}
}
async function pollEvents() {
try {
const data = await api('status');
if (!data.running) {
clearInterval(state.pollTimer);
return;
}
const events = data.events || [];
const newEvents = events.slice(state.events.length);
state.events = events;
for (const event of events) {
if (event.classifications && event.classifications.length > 0) {
for (const c of event.classifications) {
const cat = c.category;
state.categories.add(cat);
if (!state.flows[cat]) state.flows[cat] = {};
const dest = event.destination?.hostname ||
event.destination?.address || 'unknown';
state.destinations.add(dest);
if (!state.flows[cat][dest]) state.flows[cat][dest] = 0;
state.flows[cat][dest]++;
}
}
}
updateLiveStats();
updateFlowDiagram();
updateEventFeed(newEvents);
} catch (e) {
}
}
function updateRecordButton() {
const btn = document.getElementById('btnRecord');
if (state.recording) {
btn.className = 'btn-record stop';
btn.innerHTML = '<span>⏹</span> Stop Recording';
btn.disabled = false;
} else {
btn.className = 'btn-record start';
btn.innerHTML = '<span>▶</span> Start Recording';
btn.disabled = !state.selectedTarget;
}
}
function updateStatus(mode) {
const badge = document.getElementById('statusBadge');
const text = document.getElementById('statusText');
badge.className = `status-badge ${mode}`;
text.textContent = mode === 'recording' ? 'Recording' : 'Ready';
}
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function updateLiveStats() {
document.getElementById('statEvents').textContent = state.events.length;
document.getElementById('statCategories').textContent = state.categories.size;
document.getElementById('statDestinations').textContent = state.destinations.size;
}
function updateFlowDiagram() {
const container = document.getElementById('flowDiagram');
const entries = Object.entries(state.flows);
if (entries.length === 0) {
container.innerHTML = `
<div class="flow-row" style="color: var(--text-muted); justify-content: center;">
No sensitive data detected yet...
</div>
`;
return;
}
container.innerHTML = entries.map(([cat, dests]) => {
const destEntries = Object.entries(dests).sort((a, b) => b[1] - a[1]);
const destHtml = destEntries.map(([dest, count]) => {
const isThirdParty = true; return `<span class="flow-dest ${isThirdParty ? 'third-party' : 'first-party'}">${esc(dest)}</span>`;
}).join('');
const totalCount = destEntries.reduce((sum, [, c]) => sum + c, 0);
return `
<div class="flow-row">
<span class="flow-category">${formatCategory(cat)}</span>
<span class="flow-arrow">→</span>
<div class="flow-destinations">${destHtml}</div>
<span class="flow-count">${totalCount}×</span>
</div>
`;
}).join('');
}
function updateEventFeed(newEvents) {
const feed = document.getElementById('eventFeed');
for (const event of newEvents) {
const time = new Date(event.timestamp).toLocaleTimeString([], {
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
const domain = event.destination?.hostname ||
event.destination?.address || 'unknown';
const cats = (event.classifications || []).map(c =>
`<span class="event-tag">${formatCategory(c.category)}</span>`
).join('');
const method = event.kind === 'tls_plaintext' ? 'HTTPS' :
event.kind?.replace('_', '') || '???';
const el = document.createElement('div');
el.className = 'event-item';
el.innerHTML = `
<span class="event-time">${time}</span>
<span class="event-method">${method}</span>
<span class="event-domain">${esc(domain)}</span>
<div class="event-tags">${cats}</div>
`;
feed.prepend(el);
while (feed.children.length > 50) {
feed.removeChild(feed.lastChild);
}
}
}
function copyReport() {
navigator.clipboard.writeText(state.report);
}
function downloadReport() {
const blob = new Blob([state.report], { type: 'text/plain' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `daylight-report-${new Date().toISOString().slice(0, 10)}.txt`;
a.click();
}
function startNew() {
state.events = [];
state.flows = {};
state.destinations = new Set();
state.categories = new Set();
state.report = '';
state.recording = false;
state.selectedTarget = null;
updateRecordButton();
updateStatus('idle');
showScreen('screenWelcome');
loadTargets();
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
function formatKiB(kib) {
if (kib > 1024 * 1024) return (kib / 1024 / 1024).toFixed(1) + ' GiB';
if (kib > 1024) return (kib / 1024).toFixed(0) + ' MiB';
return kib + ' KiB';
}
function formatCategory(cat) {
return (cat || '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
loadTargets();
</script>
</body>
</html>