Add multi-user auth, settings area, and unified label feed

Turns Daybook into a multi-user tool and reworks the feed UI:

- Multi-user: Django session login (username/password + remember-me),
  per-user personal API tokens (DRF authtoken), whole app gated behind
  login. Each artifact has an owner; feeds are private per user.
- Rename persona -> source (free-text label for whatever posted an
  artifact: agent-profile, script, or person). source + tags form one
  unified label system; single label filter matches tags, source has
  its own filter/column/sidebar.
- Settings area (gear/user menu, top-right): Account (view/regenerate
  token) and Demo Data (in-app button seeding one artifact per kind).
  User account management via Django admin (superusers only).
- Feed rework: rolling 7-day window with sticky inline date separators
  and infinite scroll; left sidebar is now a Sources list with per-source
  unread counts; artifact type shown as text after title; source in its
  own column.
- Unread tracking: read_at on Artifact, marked read when previewed,
  bold titles for unread, sidebar unread badges, old banner removed.
- Tag colours: global shared registry, hash-based default with optional
  per-tag override via tag_colors on POST/PATCH.
- Ingest: token resolves owner; same source name under two users' tokens
  stays isolated. daybook_post.py renamed persona->source, requires a
  real token, --check accepts a bearer token. Docs updated to match.

Migrations 0004-0008 rename persona, add owner (nullable -> backfill to
first superuser -> required), and add read_at. Fresh installs no-op the
backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ritte 2026-07-24 09:06:46 +02:00
parent 203f9b578e
commit 37cd96fa7a
33 changed files with 1614 additions and 367 deletions

View file

@ -7,11 +7,13 @@ The helper is for agents and scripts that need to publish artifacts to Daybook w
Configuration:
```text
DAYBOOK_URL=http://127.0.0.1:8000
DAYBOOK_TOKEN=dev-token
DAYBOOK_PERSONA=agent
DAYBOOK_URL=http://127.0.0.1:8001
DAYBOOK_TOKEN=your-personal-api-token
DAYBOOK_SOURCE=agent
```
`DAYBOOK_TOKEN` is a personal per-user token from Settings → Account in the app, not shared across users.
Examples:
```bash

View file

@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Post one demo artifact of every kind, tagged, dated today.
Posts through the real ingest API using the existing daybook_post.py helper,
so seeded data goes through the same normalization as real agent output.
Usage (from project root, with the dev server running):
python scripts/seed_demo_artifacts.py
"""
import subprocess
import sys
import tempfile
from pathlib import Path
HELPER = Path(__file__).resolve().parent.parent / "skills" / "daybook-posting" / "scripts" / "daybook_post.py"
PERSONA = "seed-demo"
TAGS = "seed-demo,demo"
def run_post(args):
result = subprocess.run([sys.executable, str(HELPER), *args], capture_output=True, text=True)
print(result.stdout.strip())
if result.returncode != 0:
print(result.stderr.strip(), file=sys.stderr)
return result.returncode
def post_text(kind, title, content, extra_tag):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False, encoding="utf-8") as tmp:
tmp.write(content)
content_path = tmp.name
return run_post([
"--kind", kind,
"--title", title,
"--persona", PERSONA,
"--tags", f"{TAGS},{extra_tag}",
"--content-file", content_path,
])
def post_file(title, file_path, extra_tag):
return run_post([
"--file", str(file_path),
"--title", title,
"--persona", PERSONA,
"--tags", f"{TAGS},{extra_tag}",
])
def main():
exit_codes = []
exit_codes.append(post_text(
"markdown",
"Seed: Markdown Sample",
"# Markdown Sample\n\nThis is a **seed** artifact demonstrating markdown rendering.\n\n- item one\n- item two\n",
"markdown",
))
exit_codes.append(post_text(
"html_fragment",
"Seed: HTML Fragment Sample",
"<h2>HTML Fragment</h2><p>This is a seed HTML fragment artifact.</p>",
"html-fragment",
))
exit_codes.append(post_text(
"html_full",
"Seed: Full HTML Sample",
"<!doctype html><html><head><title>Seed Full HTML</title></head>"
"<body><h1>Full HTML Doc</h1><p>Seed artifact.</p></body></html>",
"html-full",
))
exit_codes.append(post_text(
"deck",
"Seed: Deck Sample",
"<section><h1>Slide One</h1><p>Seed deck artifact.</p></section>"
"<section><h1>Slide Two</h1><p>More content.</p></section>",
"deck",
))
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir)
png_path = tmp_dir / "seed_pixel.png"
png_path.write_bytes(bytes.fromhex(
"89504e470d0a1a0a0000000d494844520000000100000001080600000"
"01f15c4890000000a49444154789c6360000002000155e59a1a0000000049454e44ae426082"
))
exit_codes.append(post_file("Seed: Image Sample", png_path, "image"))
pdf_path = tmp_dir / "seed_doc.pdf"
pdf_path.write_bytes(
b"%PDF-1.1\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
b"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
b"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]>>endobj\n"
b"trailer<</Root 1 0 R>>\n"
)
exit_codes.append(post_file("Seed: PDF Sample", pdf_path, "pdf"))
bin_path = tmp_dir / "seed_data.bin"
bin_path.write_bytes(b"\x00\x01\x02\x03seed-binary-data\xff\xfe")
exit_codes.append(post_file("Seed: Binary Sample", bin_path, "binary"))
return 1 if any(exit_codes) else 0
if __name__ == "__main__":
raise SystemExit(main())