Initial Daybook MVP

This commit is contained in:
peter 2026-06-10 15:48:19 +01:00
commit 3974276c9b
25 changed files with 1647 additions and 0 deletions

View file

@ -0,0 +1,275 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daybook</title>
<link rel="stylesheet" href="/static/daybook.css">
</head>
<body>
<header class="topbar">
<div class="brandline">
<h1>Daybook</h1>
<p>Agent Artifacts</p>
</div>
<div class="toolbar">
<form method="get" class="filter">
<input type="hidden" name="day" value="{{ selected_day|date:'Y-m-d' }}">
<input type="hidden" name="view" value="{{ selected_view }}">
{% if selected_artifact %}<input type="hidden" name="artifact" value="{{ selected_artifact.id }}">{% endif %}
<label>
<span>Persona</span>
<select name="persona" onchange="this.form.submit()">
<option value="">All personas</option>
{% for persona in personas %}
<option value="{{ persona }}" {% if persona == selected_persona %}selected{% endif %}>{{ persona }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Type</span>
<select name="kind" onchange="this.form.submit()">
<option value="">All types</option>
{% for value, label in kind_choices %}
<option value="{{ value }}" {% if value == selected_kind %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<label>
<span>Name</span>
<input type="search" name="q" value="{{ title_query }}" placeholder="Title contains" onsearch="this.form.submit()">
</label>
<button type="submit">Filter</button>
</form>
</div>
</header>
<main class="feed">
<aside class="day-sidebar" aria-label="Days">
<h2>Days</h2>
<nav class="day-list">
{% for day in days %}
<a class="day-link {% if day == selected_day %}selected{% endif %}" href="?day={{ day|date:'Y-m-d' }}{% if selected_persona %}&amp;persona={{ selected_persona|urlencode }}{% endif %}{% if selected_kind %}&amp;kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&amp;q={{ title_query|urlencode }}{% endif %}&amp;view={{ selected_view }}">
<span>{{ day|date:"D" }}</span>
<strong>{{ day|date:"Y-m-d" }}</strong>
</a>
{% endfor %}
</nav>
</aside>
<div class="splitter" data-splitter="days" role="separator" aria-label="Resize days sidebar" aria-orientation="vertical"></div>
<aside class="artifact-sidebar" aria-label="Artifacts for selected day">
<div class="pane-heading">
<h2>{{ selected_day|date:"Y-m-d" }}</h2>
<span>{{ artifacts|length }} artifact{{ artifacts|length|pluralize }}</span>
</div>
<div class="artifact-list">
{% for artifact in artifacts %}
<a class="artifact-row {% if selected_artifact.id == artifact.id %}selected{% endif %}"
href="?day={{ selected_day|date:'Y-m-d' }}{% if selected_persona %}&amp;persona={{ selected_persona|urlencode }}{% endif %}{% if selected_kind %}&amp;kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&amp;q={{ title_query|urlencode }}{% endif %}&amp;artifact={{ artifact.id }}&amp;view={{ selected_view }}"
data-artifact-id="{{ artifact.id }}"
data-title="{{ artifact.title }}"
data-kind="{{ artifact.kind }}"
data-rendered-url="{% url 'artifact' artifact.id %}"
data-raw-url="{% url 'artifact' artifact.id %}?raw=1">
<span class="kind-icon kind-{{ artifact.kind }}" aria-hidden="true">
{% if artifact.kind == "image" %}IMG{% elif artifact.kind == "pdf" %}PDF{% elif artifact.kind == "markdown" %}MD{% elif artifact.kind == "deck" %}DECK{% elif artifact.kind == "binary" %}BIN{% else %}HTML{% endif %}
</span>
<span class="artifact-row-main">
<strong>{{ artifact.created_at|date:"H:i" }}</strong>
<span>{{ artifact.title }}</span>
</span>
</a>
{% empty %}
<div class="empty compact">
<h3>No artifacts</h3>
<p>This day is ready for new agent output.</p>
</div>
{% endfor %}
</div>
</aside>
<div class="splitter" data-splitter="artifacts" role="separator" aria-label="Resize artifacts sidebar" aria-orientation="vertical"></div>
<section class="preview-pane">
{% if selected_artifact %}
<div class="preview-frame-wrap">
{% if selected_view == "raw" %}
<iframe class="preview-frame" src="{% url 'artifact' selected_artifact.id %}?raw=1" title="{{ selected_artifact.title }} raw source"></iframe>
{% elif selected_artifact.kind == "image" %}
<img class="preview-image" src="{% url 'artifact' selected_artifact.id %}" alt="{{ selected_artifact.title }}">
{% elif selected_artifact.kind == "binary" %}
<div class="empty preview-empty">
<h2>Binary Artifact</h2>
<p>This artifact type cannot be previewed inline yet.</p>
<p><a href="{% url 'artifact' selected_artifact.id %}">Open or download it</a>.</p>
</div>
{% else %}
<iframe class="preview-frame" src="{% url 'artifact' selected_artifact.id %}{% if selected_view == 'raw' %}?raw=1{% endif %}" title="{{ selected_artifact.title }}"></iframe>
{% endif %}
</div>
<footer class="artifact-actions" data-actions>
<nav class="view-toggle" aria-label="Preview mode">
<a class="{% if selected_view == 'rendered' %}selected{% endif %}" href="{{ rendered_url }}" data-view="rendered">Rendered</a>
<a class="{% if selected_view == 'raw' %}selected{% endif %}" href="{{ raw_url }}" data-view="raw">Raw</a>
</nav>
<div class="artifact-action-links">
<a href="{% url 'artifact' selected_artifact.id %}" target="_blank" rel="noopener">Open</a>
<a href="{% url 'artifact' selected_artifact.id %}" download>Download</a>
<button type="button" data-copy-url="{% url 'artifact' selected_artifact.id %}">Copy Link</button>
<button type="button" disabled>Send</button>
</div>
</footer>
{% else %}
<div class="empty preview-empty">
<h2>Select an artifact</h2>
<p>Choose an item from the middle column to preview it here.</p>
<p>POST to <code>/api/artifacts</code> with <code>Authorization: Bearer dev-token</code>.</p>
</div>
{% endif %}
</section>
</main>
<script>
(() => {
const feed = document.querySelector(".feed");
if (!feed) return;
let selectedView = "{{ selected_view }}";
const sizes = {
days: Number(localStorage.getItem("daybook.daysWidth")) || 144,
artifacts: Number(localStorage.getItem("daybook.artifactsWidth")) || 280,
};
const applySizes = () => {
feed.style.setProperty("--days-width", `${sizes.days}px`);
feed.style.setProperty("--artifacts-width", `${sizes.artifacts}px`);
};
applySizes();
document.querySelectorAll(".splitter").forEach((splitter) => {
splitter.addEventListener("pointerdown", (event) => {
const target = splitter.dataset.splitter;
const startX = event.clientX;
const startWidth = sizes[target];
splitter.setPointerCapture(event.pointerId);
document.body.classList.add("is-resizing");
const move = (moveEvent) => {
sizes[target] = Math.max(96, Math.min(420, startWidth + moveEvent.clientX - startX));
applySizes();
};
const up = () => {
localStorage.setItem(`daybook.${target}Width`, String(sizes[target]));
document.body.classList.remove("is-resizing");
splitter.removeEventListener("pointermove", move);
splitter.removeEventListener("pointerup", up);
splitter.removeEventListener("pointercancel", up);
};
splitter.addEventListener("pointermove", move);
splitter.addEventListener("pointerup", up);
splitter.addEventListener("pointercancel", up);
});
});
const previewPane = document.querySelector(".preview-pane");
const rows = Array.from(document.querySelectorAll(".artifact-row"));
const escapeHtml = (value) => value.replace(/[&<>"]/g, (character) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
}[character]));
const copyLink = async (button) => {
const absoluteUrl = new URL(button.dataset.copyUrl, window.location.origin).toString();
try {
await navigator.clipboard.writeText(absoluteUrl);
} catch {
window.prompt("Copy artifact link", absoluteUrl);
}
const original = button.textContent;
button.textContent = "Copied";
setTimeout(() => {
button.textContent = original;
}, 1200);
};
const selectedRow = () => document.querySelector(".artifact-row.selected");
const urlFor = (row, view) => view === "raw" ? row.dataset.rawUrl : row.dataset.renderedUrl;
const pageUrlFor = (row, view) => {
const url = new URL(row.href, window.location.origin);
url.searchParams.set("artifact", row.dataset.artifactId);
url.searchParams.set("view", view);
return url;
};
const updateActions = (row) => {
const actions = document.querySelector("[data-actions]");
if (!actions) return;
actions.querySelectorAll("[data-view]").forEach((link) => {
const view = link.dataset.view;
link.classList.toggle("selected", view === selectedView);
link.href = pageUrlFor(row, view).toString();
});
actions.querySelectorAll(".artifact-action-links a").forEach((link) => {
link.href = row.dataset.renderedUrl;
});
const copyButton = actions.querySelector("[data-copy-url]");
if (copyButton) copyButton.dataset.copyUrl = row.dataset.renderedUrl;
};
const renderPreview = (row, pushState = true) => {
rows.forEach((candidate) => candidate.classList.toggle("selected", candidate === row));
const source = urlFor(row, selectedView);
const title = escapeHtml(row.dataset.title);
const kind = row.dataset.kind;
const content = selectedView === "raw"
? `<iframe class="preview-frame" src="${source}" title="${title} raw source"></iframe>`
: kind === "image"
? `<img class="preview-image" src="${source}" alt="${title}">`
: kind === "binary"
? `<div class="empty preview-empty"><h2>Binary Artifact</h2><p>This artifact type cannot be previewed inline yet.</p><p><a href="${source}">Open or download it</a>.</p></div>`
: `<iframe class="preview-frame" src="${source}" title="${title}"></iframe>`;
let frameWrap = previewPane.querySelector(".preview-frame-wrap");
if (!frameWrap) {
previewPane.innerHTML = `<div class="preview-frame-wrap"></div><footer class="artifact-actions" data-actions><nav class="view-toggle" aria-label="Preview mode"><a data-view="rendered">Rendered</a><a data-view="raw">Raw</a></nav><div class="artifact-action-links"><a target="_blank" rel="noopener">Open</a><a download>Download</a><button type="button" data-copy-url="">Copy Link</button><button type="button" disabled>Send</button></div></footer>`;
frameWrap = previewPane.querySelector(".preview-frame-wrap");
}
frameWrap.innerHTML = content;
updateActions(row);
if (pushState) window.history.pushState({}, "", pageUrlFor(row, selectedView));
};
rows.forEach((row) => {
row.addEventListener("click", (event) => {
event.preventDefault();
renderPreview(row);
});
});
previewPane.addEventListener("click", (event) => {
const copyButton = event.target.closest("[data-copy-url]");
if (copyButton) {
event.preventDefault();
copyLink(copyButton);
return;
}
const toggle = event.target.closest("[data-view]");
if (!toggle) return;
const row = selectedRow();
if (!row) return;
event.preventDefault();
selectedView = toggle.dataset.view;
renderPreview(row);
});
})();
</script>
</body>
</html>