- Sidebar sources ordered by Max(created_at) desc instead of alphabetically;
"All sources" stays pinned at top (rendered separately).
- Live updates move a source to the top when it gets a new message, so the
recency order holds without a reload.
152 tests pass; JS re-validated with node --check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move the Keep/Deleted system folders out of the scrolling source list into a
pinned section at the bottom of the sidebar (only sources scroll now), so the
folders stay reachable with many sources. Renamed .source-folders ->
.system-folders; .source-list flexes to fill.
- "All sources" is visually distinct (bold + subtle blue tint) as the primary
entry.
Template/CSS only. Feed/trash rendering tests pass; feed renders in all views.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A ⋯ button on row hover and right-click on a feed row open the same floating
menu: Keep / Open / Copy link / Delete. Dismisses on click-away/Escape,
edge-flips to stay on-screen. Standalone one-click Delete icon stays; the
Trash view keeps its dedicated Undelete.
- Row markup: actions grouped in a .row-actions container (delete + ⋯ in the
feed/keep views; undelete in trash). Reuses the existing keep endpoint.
- Menu only in the normal/keep views, not trash. Keep-toggle syncs the row's
kept badge + the preview Keep button, and (in the Keep folder) drops the row
when unkept.
- Frontend-only (no backend change); inline JS validated with node --check;
152 tests pass; feed renders in all views.
Share + Add-label join the menu in later slices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trashed artifacts past a ~3-day retention window are now purged automatically.
- storage.purge_expired_trash(): removes trash older than TRASH_RETENTION
(all users), tolerant of already-gone rows (concurrent-safe).
- spawn_trash_purge(): runs it on a daemon thread (manages/closes its own DB
connection), triggered by each delete so it's cron-less and off the request
thread. Inline fallback when AGENTFEED_ASYNC_PURGE is off (tests).
- Wired into both delete paths (API + session). Manual Empty Trash unchanged.
- Tests: purge removes expired-only; delete triggers the purge. 152 passing;
live delete verified with the thread enabled (204, server healthy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete now moves an artifact to a recoverable Trash instead of hard-removing.
- Model: Artifact.deleted_at (nullable, indexed) = in Trash.
- Soft-delete everywhere: DELETE /api/artifacts/{id} sets deleted_at
(idempotent 204); session POST /a/{id}/delete for the feed's hover Delete
icon; POST /a/{id}/undelete restores; POST /feed/empty-trash purges.
- Trashed artifacts are off-quota (deleting frees quota) and eviction-protected
(never evicted early); hidden from feed, sources, counts, and all API reads
(list/detail/raw).
- Sidebar: "Deleted" and "Keep" system folders at the bottom (counts, hidden
when empty); ?folder=deleted / ?folder=keep views reuse the row rendering.
Deleted view has hover Undelete + an Empty Trash button. Keep = view over
is_kept. Live-poll disabled in folder views.
- Docs (api.md delete section, CLAUDE.md), 10 new/updated tests. 150 passing;
API soft-delete lifecycle verified live.
Purge is manual (Empty Trash) this release; auto purge-on-delete (background,
~3-day retention) is the planned fast-follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends v0.4.0 without a second request: the sidebar is updated client-side
from the rows the existing /feed/updates poll already returns.
- Each new row's data-source drives the sidebar: per-source + "All sources"
unread counts bump; a first-time source is inserted into the list
(alphabetically). Reuses the existing count-badge pattern.
- sourceLinks is now live-queried (was cached at load) so sources added live
stay keyboard-navigable.
- No endpoint/model change. 142 tests pass; feed renders.
Caveat: while filtered to a single source, other new sources appear only on
reload (the poll is scoped to the selected view).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New artifacts appear at the top of the feed without a manual refresh.
- GET /feed/updates (session-authed): new artifacts since a timestamp marker,
owner-scoped and filter-aware (source/label/kind/q), returned as row HTML.
- Feed JS polls every 5s, paused while the tab is hidden (immediate poll on
refocus). Prepends new rows into today's group without touching the selected
row or the preview iframe — what you're reading stays put and selected.
- Fresh rows show "now", aging into their real time after ~3 min; brief
highlight on arrival.
- Extracted _artifact_rows.html partial, shared by the day-group render and the
updates endpoint, so live rows match exactly.
- Polling chosen over SSE/WebSockets: zero infra on sync-WSGI + SQLite (see
planning/next_steps for the SSE upgrade path). 142 tests pass; feed page +
endpoint verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Metadata (GET /api/artifacts/{id}) stays metadata-only; the full content now
has its own endpoint so an agent can verify exactly what it posted.
- New ArtifactRawView at /api/artifacts/{id}/raw: bearer-or-session auth,
owner-scoped. Serves exact source text (markdown/html_full/json/csv), file
bytes (image/pdf/binary), or 404 for render-only kinds (html_fragment/deck).
Reuses raw_artifact_path/safe_artifact_path.
- serialize_artifact exposes api_raw_url; /api/capabilities advertises the
route. (The old /a/{id}?raw= viewer stays session-only for browsers.)
- Docs + tests (exact content, owner-scoping, 403 unauth, 404 render-only).
Suite: 136 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes an inconsistency an agent hit: it could POST/PATCH/DELETE with its token
but GET (readback) required a session and 302-redirected to the browser login.
- GET /api/artifacts and GET /api/artifacts/{id} now accept a bearer token OR
a session, owner-scoped (a token reads only its owner's artifacts). Agents
can read back what they posted with the same token.
- Unauthenticated API requests return a JSON 403, never a browser-login
redirect (resolve_request_user helper; dropped @login_required on the JSON
read views).
- GET /api/skill and GET /api/capabilities are now public (no user data), so
an agent can fetch the contract + posting instructions before it has a key.
- Docs (api.md, CLAUDE.md) updated; tests for bearer readback, owner-scoping,
and the JSON-403-not-redirect behavior. Suite: 132 passing; verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the "Daybook" name from everything user- and agent-facing, since no
agents are configured yet and this is the cheap moment to do it.
- UI brand, docs, README, CLAUDE.md -> AgentFeed.
- Ingest env vars DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/BIND_ADDR/HOST_DATA_DIR/
KEY_PROVISIONING_API -> AGENTFEED_* (token *values* unchanged).
- Skill: skills/daybook-posting -> skills/agentfeed-posting, daybook_post.py
-> agentfeed_post.py (+ scripts/ wrapper and its test).
- static/daybook.css -> static/agentfeed.css; localStorage daybook.* ->
agentfeed.*; templatetag daybook_extras -> agentfeed_extras; multipart
boundaries; json/csv viewer element ids; daybook_exception_handler ->
agentfeed_exception_handler; compose service/container -> agentfeed.
- Key-provisioning API now OFF by default (independent of DEBUG); enable with
AGENTFEED_KEY_PROVISIONING_API=1 on test installs only.
- Posting helper now points at Settings -> API Keys.
Deferred on purpose (a later, separate step): the Django package
`daybook_project`, the `/opt/daybook` install path, and the Forgejo repo name.
The sibling AGENT_DAYBOOK__LLM_TEST name is intentionally untouched.
Full suite: 129 passing; live server verified serving under the new name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace the gear/username button with a round initials avatar (per-user
deterministic color via a new daybook_extras template filter); dropdown
now shows the username and keeps Settings/About/theme/logout. Sets up an
optional profile photo later.
- Regroup the settings nav into "You" (Account, API Keys, Storage, Demo
Data) and a superuser-only "Admin" (Users), About separate — the split
now reflects who each setting affects, not a flat list.
- Register ApiKey (key read-only) and TagColor in Django admin so /admin/
is complete as the operator break-glass tool, distinct from the themed
Settings product surface.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the single per-user DRF token with named, individually-revocable
API keys, so you can run one key per agent and cut off a leaked one without
disrupting the others.
- New ApiKey model (user, name, dbk_-prefixed key, created_at, last_used_at).
Data migration copies every existing authtoken 1:1 into a "default" named
key with the same value, so no client (incl. the sibling test project's
hardcoded test-user key) needs reconfiguring.
- Auth (resolve_token_user) resolves against ApiKey and stamps last_used_at,
throttled to one write/minute.
- Settings -> API Keys: table with name/key/created/last-used + per-row
revoke and a create form; keys viewable on demand. Settings -> Account now
links here; Settings -> Users issues a named initial key.
- Gated provisioning API for test installations: GET/POST /api/keys and
DELETE /api/keys/{id}, controlled by DAYBOOK_KEY_PROVISIONING_API (unset ->
follows DEBUG, so off in deployment). Advertised via key_provisioning_api
in /api/capabilities; 403 when disabled.
- Docs (api/deployment/quickstart/posting-helper), SKILL.md, and env.example
updated from "token / Settings -> Account" to "key / Settings -> API Keys".
- Tests: new API + UI suites for keys; existing token-based tests moved to
ApiKey. Full suite 129 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the deploy blocker found deploying v0.1.0 to cloud-vm-1: the vendored
gridjs.umd.js ended with a //# sourceMappingURL=gridjs.umd.js.map trailer,
but that .map isn't in the repo. The strict CompressedManifestStaticFilesStorage
backend follows the reference and raises MissingFileError, aborting the
entrypoint's collectstatic so the container never served. Stripped the
debug-only trailer (no runtime effect); collectstatic now succeeds from a
pristine checkout (169 copied, 481 post-processed).
Also rolls up this release's already-committed work: GET /api/skill and the
location-agnostic posting-skill rewrite.
- Bump __version__ to 0.1.1.
- Runbook: note that a post-build crash-loop is most likely a collectstatic
MissingFileError (check docker logs), and how to clear on-server tracked
drift before checking out a tag.
- CHANGELOG: 0.1.1 entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single source of truth: __version__ in daybook_project/__init__.py, starting
at 0.1.0. Surfaced in two places from that constant:
- Settings -> About page (logo, name, version, description), in the settings
nav and linked from the gear menu.
- GET /api/capabilities now includes "version", so the deploy agent / tester
can confirm which version is actually running.
Release flow: bump __version__, commit, `git tag vX.Y.Z`, push tag, deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#015 — deeply nested JSON raised RecursionError (a RuntimeError, not
ValueError) and became an unhandled 500 on two paths:
- json-kind content validation now catches RecursionError -> 400.
- Added a custom DRF EXCEPTION_HANDLER
(artifacts/exception_handlers.py, wired via REST_FRAMEWORK) mapping
otherwise-unhandled RecursionError -> 400, covering the parser-level
crash on deeply nested request bodies (metadata etc.) too.
#016 — wrap_csv_viewer handed Grid.js rows without reconciling width to the
header, so over-long rows silently dropped cells. Now normalizes every row
to the max width: long rows widen the columns (auto-named "Column N"),
short rows pad with "". No cell is dropped.
Both verified live; 4 regression tests added. 106 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
make_room_for() read usage before any write lock was held, so concurrent
requests could each read stale data and both conclude "this fits" -
confirmed reproducible (measured 616 bytes over a 1551-byte quota with 5
truly concurrent posts).
Fix: force SQLite to acquire its write lock immediately via a no-op UPDATE
on the user's UserProfile row, before reading `used`, wrapped in
transaction.atomic() through the subsequent Artifact.objects.create() so
the lock is held across the whole eviction+insert sequence. Added a 20s
DB timeout as headroom for legitimate contention.
Deliberately no retry/backoff logic - this project's actual load (a
low-volume self-hosted tool) doesn't need it, and SQLite has no row-level
locking regardless, so quota-affecting writes serialize globally either
way. Did fix a real bug found while verifying this: exception handling
around artifact creation only caught QuotaExceeded, so a genuine lock
timeout left the just-written file orphaned on disk - both call sites now
clean up on any exception.
Verified live against the real dev instance (twice, with 5 truly
simultaneous posts against a tight quota) rather than via an automated
test - Django's in-memory shared-cache test database uses a different,
non-retryable SQLite locking model that fails for reasons unrelated to
this fix's correctness.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Django had no CSRF_TRUSTED_ORIGINS and no SECURE_PROXY_SSL_HEADER, so
it always treated requests as plain HTTP. Login over HTTPS behind
Apache/Nginx failed CSRF verification as a result.
Daybook: a self-hosted, multi-user Django + DRF app where agents, scripts,
or people publish artifacts and each logged-in user browses their own
time-ordered, label-filterable feed.
This squashes prior local development history (kept privately on a separate
branch) into a single clean starting point for the public repository.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>