Compare commits

..

No commits in common. "v0.1.1" and "v0.1.0" have entirely different histories.

9 changed files with 42 additions and 305 deletions

View file

@ -1,68 +0,0 @@
# Changelog
All notable changes to Daybook are recorded here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/); versions are SemVer and match
the git tags (`vX.Y.Z`) and `daybook_project.__version__`.
## [Unreleased]
_Nothing yet._
## [0.1.1] — 2026-09-03
### Fixed
- Deployment crash-loop: the vendored `gridjs.umd.js` referenced a
`gridjs.umd.js.map` sourcemap that isn't in the repo, so `collectstatic`
(strict manifest storage) failed on container start and v0.1.0 never came
up. Removed the dangling sourcemap trailer (debug-only, no runtime effect).
### Added
- `GET /api/skill` serves the bundled agent-posting skill as Markdown (or
`?format=json`), with `DAYBOOK_URL` pre-filled to the instance's own
origin so the instructions are ready to hand to an AI. Advertised as
`skill_url` in `/api/capabilities`.
### Changed
- Rewrote the posting skill (`skills/daybook-posting/SKILL.md`) to be
location-agnostic: no longer assumes a local source checkout, reframes
`DAYBOOK_URL` as set-your-instance, and adds a raw `curl` fallback.
## [0.1.0] — 2026-09-03
First tagged release.
### Artifacts & rendering
- Artifact kinds: `markdown`, `html_fragment`, `html_full`, `deck`, `json`,
`csv`, `image`, `pdf`, `binary`.
- `.xlsx` uploads convert server-side to a `csv` artifact (active sheet,
saved formula results).
- Markdown code blocks get syntax highlighting; tables (markdown &
html_fragment) render styled — monospace cells, sans-serif headers.
- `json`/`csv` render as an interactive collapsible tree / sortable table
(Grid.js + renderjson, vendored locally).
- Unified off-white document background; system sans-serif body text.
### API
- `POST/GET/PATCH/DELETE /api/artifacts`, list filters (source/label/day/
kind/q), `/api/assets` image upload, `/api/capabilities` (reports
`version`). Per-user bearer-token ingest; session-authed reads.
- Server-side validation: per-kind content shape, field length limits,
string-typed source fields, valid-JSON for `json`.
### Sharing, quotas, security
- Per-artifact public share links (`/s/{token}`), owner-toggled, revocable.
- Per-user disk quotas with oldest-unkept auto-eviction and a Keep flag.
- Allowlist HTML sanitization (nh3) + `script-src 'none'` CSP for the themed
kinds; deep-JSON parse errors return 400 (not 500).
### UI
- Multi-user auth; feed with source sidebar + label filtering; settings area
(Account, Storage, Demo Data, Users, About); light/dark toggle; app icon.
### Deployment
- Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a
runbook (`deploy/cloud_agent_deploy.md`) and health check.
[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...HEAD
[0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1
[0.1.0]: https://git.cygnus.systems/peter/daybook/releases/tag/v0.1.0

View file

@ -14,7 +14,6 @@ urlpatterns = [
path("assets/<str:asset_id>", views.asset_view, name="asset"), path("assets/<str:asset_id>", views.asset_view, name="asset"),
path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-api"), path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-api"),
path("api/capabilities", views.CapabilitiesView.as_view(), name="capabilities-api"), path("api/capabilities", views.CapabilitiesView.as_view(), name="capabilities-api"),
path("api/skill", views.SkillView.as_view(), name="skill-api"),
path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"), path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"),
path("api/artifacts/<str:artifact_id>", views.ArtifactDetailView.as_view(), name="artifact-detail-api"), path("api/artifacts/<str:artifact_id>", views.ArtifactDetailView.as_view(), name="artifact-detail-api"),
path("settings/", views.settings_home, name="settings-home"), path("settings/", views.settings_home, name="settings-home"),

View file

@ -322,57 +322,11 @@ class CapabilitiesView(APIView):
"/api/artifacts": ["GET", "POST"], "/api/artifacts": ["GET", "POST"],
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
"/api/assets": ["POST"], "/api/assets": ["POST"],
"/api/capabilities": ["GET"],
"/api/skill": ["GET"],
}, },
"skill_url": request.build_absolute_uri(reverse("skill-api")),
} }
) )
SKILL_MD_PATH = settings.BASE_DIR / "skills" / "daybook-posting" / "SKILL.md"
def render_posting_skill(origin):
"""Return the bundled posting SKILL.md with its DAYBOOK_URL placeholder
rewritten to this instance's origin, so text handed to an agent already
points at the right server. Only the config-block assignment is touched;
the surrounding prose (which explains DAYBOOK_URL) is left alone."""
text = SKILL_MD_PATH.read_text(encoding="utf-8")
return re.sub(
r"(?m)^(DAYBOOK_URL=)\S+",
lambda m: m.group(1) + origin,
text,
)
class SkillView(APIView):
"""Serve the bundled agent-posting skill (skills/daybook-posting/SKILL.md),
with DAYBOOK_URL pre-filled to this instance so a recipient posts to the
right server. Same auth as capabilities: a logged-in user can copy it, or
an agent can fetch it with its API token.
GET /api/skill -> raw Markdown (text/markdown)
GET /api/skill?format=json -> {"version", "url", "skill"}
"""
def get(self, request):
if not request.user.is_authenticated and resolve_token_user(request) is None:
return HttpResponseForbidden("Login or a valid API token is required")
origin = request.build_absolute_uri("/").rstrip("/")
try:
skill = render_posting_skill(origin)
except FileNotFoundError:
raise Http404("Posting skill is not bundled with this deployment")
if request.GET.get("format") == "json":
from daybook_project import __version__
return Response({"version": __version__, "url": origin, "skill": skill})
return HttpResponse(skill, content_type="text/markdown; charset=utf-8")
FEED_WINDOW_DAYS = 7 FEED_WINDOW_DAYS = 7

View file

@ -1,4 +1,4 @@
# Single source of truth for the app version. Bump this, commit, then tag the # Single source of truth for the app version. Bump this, commit, then tag the
# commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in # commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in
# GET /api/capabilities ("version"). # GET /api/capabilities ("version").
__version__ = "0.1.1" __version__ = "0.1.0"

View file

@ -202,52 +202,30 @@ Confirm the response includes `id`, `url`, and `api_url`, and that opening
## 7. Redeploy Loop (routine updates) ## 7. Redeploy Loop (routine updates)
Releases are tagged `vX.Y.Z` (SemVer). Deploy a specific tag rather than a This is the command you run every time you're asked to ship a new version:
moving branch, so what's live is always a known, named version — and note
the currently-deployed one before switching, for a clean rollback target:
```bash ```bash
cd /opt/daybook cd /opt/daybook
PREV_TAG=$(git describe --tags --exact-match 2>/dev/null || git rev-parse HEAD) # what's live now PREV_SHA=$(git rev-parse HEAD)
git fetch --tags git pull
git checkout v0.1.0 # the tag you were told to deploy (latest: git tag -l | sort -V | tail -1)
docker compose -f deploy/docker-compose.yml up -d --build docker compose -f deploy/docker-compose.yml up -d --build
``` ```
Then repeat the verification in step 6 (and confirm `/api/capabilities` Then repeat the verification in step 6 (capabilities check at minimum). If it
reports the `version` you expect). If it fails, roll back to the previous fails:
version and report the failure (build log / migration error / failed health
check) rather than retrying blindly:
```bash ```bash
git checkout "$PREV_TAG" git checkout "$PREV_SHA"
docker compose -f deploy/docker-compose.yml up -d --build docker compose -f deploy/docker-compose.yml up -d --build
``` ```
Caveat: rollback reverts code, not data — a release that already applied a to roll back, then report the failure (build log / migration error / failed
DB migration may not cleanly downgrade against the migrated `db.sqlite3`. health check) back to the operator rather than retrying blindly.
For a routine forward release that's fine; if a rollback is needed after a
migration, restore the pre-deploy `deploy/data` backup (see §9) alongside
the code checkout.
`deploy/.env` and the data directory (wherever `DAYBOOK_HOST_DATA_DIR` points, `deploy/.env` and the data directory (wherever `DAYBOOK_HOST_DATA_DIR` points,
or `deploy/data` by default) are untracked, so `git pull` never touches them — or `deploy/data` by default) are untracked, so `git pull` never touches them —
no need to back them up before a routine redeploy. no need to back them up before a routine redeploy.
**If the container crash-loops right after a build** (`docker compose ps`
shows `restarting`, health never goes green), the most likely cause is the
entrypoint's `collectstatic` step failing — check `docker logs daybook`
before assuming an env/network problem. A `whitenoise ... MissingFileError`
there means a static asset references a file that isn't present (this is what
bit v0.1.0). No data or schema impact; fix the asset in the repo, tag, and
redeploy.
**If `git checkout <tag>` refuses because of local working-tree changes** to
tracked files (e.g. a previous on-server hotfix), reset those tracked files
first — `git checkout -- <file>` for a specific one, or `git stash` /
`git reset --hard` to clear all tracked drift. This never touches `deploy/.env`
or the data directory, which are untracked and therefore safe.
## 8. Hardening For Internet-Facing Deployments ## 8. Hardening For Internet-Facing Deployments
If this VM is reachable from the public internet (not just a private If this VM is reachable from the public internet (not just a private

View file

@ -529,11 +529,8 @@ Example response:
"methods": { "methods": {
"/api/artifacts": ["GET", "POST"], "/api/artifacts": ["GET", "POST"],
"/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"],
"/api/assets": ["POST"], "/api/assets": ["POST"]
"/api/capabilities": ["GET"], }
"/api/skill": ["GET"]
},
"skill_url": "http://127.0.0.1:8001/api/skill"
} }
``` ```
@ -548,33 +545,6 @@ endpoints, it accepts a bearer token as well as a browser session:
curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/capabilities curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/capabilities
``` ```
## Posting Skill
Endpoint:
```http
GET /api/skill
GET /api/skill?format=json
```
Returns the bundled agent-posting skill (`skills/daybook-posting/SKILL.md`) —
the same instructions you'd give an AI so it knows how to publish to this
instance. The server rewrites the `DAYBOOK_URL` line to **this instance's own
origin**, so the text a caller receives already points at the right server;
a recipient only has to fill in their `DAYBOOK_TOKEN`.
Same auth as capabilities (a browser session **or** a bearer token), so a
logged-in user can copy it to hand to their agent, or an agent can fetch it
directly. The default response is raw Markdown (`text/markdown`); add
`?format=json` for `{"version", "url", "skill"}`.
```bash
# Fetch the ready-to-use instructions for this instance
curl -H "Authorization: Bearer <your-personal-token>" http://127.0.0.1:8001/api/skill
```
Discover it programmatically via `skill_url` in the capabilities response.
## Current MVP Constraints ## Current MVP Constraints
- Each user has a personal ingest API token; there is no shared token. - Each user has a personal ingest API token; there is no shared token.

View file

@ -1,13 +1,26 @@
--- ---
name: daybook-posting name: daybook-posting
description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to a Daybook instance and returns a URL. description: Use when you have produced a finished deliverable such as a report, summary, brief, diagram, generated HTML, PDF, deck, or status update that a human should review later outside this chat. Publishes the artifact to Daybook and returns a URL.
--- ---
# Daybook Posting # Daybook Posting
Daybook is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations. Daybook is a private artifact journal for completed agent outputs. It stores, indexes, and displays artifacts for human review. It does not execute commands, run workflows, or continue conversations.
You post to a **running Daybook server**, which may be anywhere — a cloud VM, a container, or a workstation. You do **not** need a checkout of Daybook's source. The only things that tie you to a particular instance are two environment variables: `DAYBOOK_URL` (where the server is) and `DAYBOOK_TOKEN` (who you are). Set those and the rest of this skill works unchanged regardless of where you or the server run. Always use the helper script. Do not hand-build curl, JSON, PowerShell multipart, or inline shell content.
From the project root:
```bash
python skills/daybook-posting/scripts/daybook_post.py --check
python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report" --content-file report.md
cat report.md | python skills/daybook-posting/scripts/daybook_post.py --kind markdown --title "Build Report"
python skills/daybook-posting/scripts/daybook_post.py --file diagram.svg --title "Architecture Diagram"
```
The project also keeps a compatibility wrapper at `scripts/daybook_post.py`, but the canonical helper is bundled inside this skill.
The artifact URL is printed as the final stdout line.
## Configuration ## Configuration
@ -17,54 +30,12 @@ config) — the helper reads `os.environ` directly. There is no `.env` file
support; a `.env` file in the working directory is not read automatically. support; a `.env` file in the working directory is not read automatically.
```text ```text
DAYBOOK_URL=https://your-daybook-instance.example.com # your server's base URL DAYBOOK_URL=http://127.0.0.1:8001
DAYBOOK_TOKEN=your-personal-api-token # from Settings -> Account DAYBOOK_TOKEN=your-personal-api-token
DAYBOOK_SOURCE=agent # a stable label for what's posting DAYBOOK_SOURCE=agent
``` ```
- **`DAYBOOK_URL`** — the base URL of the Daybook instance you're posting to. `DAYBOOK_TOKEN` is your personal per-user API token, from Settings → Account in the app (not a token shared by everyone). Artifacts posted with your token land only in your feed.
Set it to your server. If it's unset, the helper falls back to
`http://127.0.0.1:8001`, which is only correct when Daybook is running
locally on the same machine (its development default). For any remote or
deployed instance you **must** set this.
- **`DAYBOOK_TOKEN`** — your personal per-user API token, from Settings →
Account in the app (not a token shared by everyone). Artifacts posted with
your token land only in your feed.
- **`DAYBOOK_SOURCE`** — a free-text label for whatever is posting
(agent-profile, script, or person name). Optional; can also be passed per
call with `--source`.
## The Helper
Always use the bundled helper script. Do not hand-build curl, JSON,
PowerShell multipart, or inline shell content (a `curl` fallback for
environments without the helper is documented at the end — the script is
still preferred everywhere it's available).
The helper lives at **`scripts/daybook_post.py` inside this skill's own
directory** — not in any project you happen to be working in. Invoke it by
its absolute path. For brevity, the examples below assume you've captured
that path once:
```bash
# Point this at the helper inside THIS skill directory (absolute path).
DAYBOOK_POST=/abs/path/to/this/skill/scripts/daybook_post.py
```
Then:
```bash
python "$DAYBOOK_POST" --check
python "$DAYBOOK_POST" --kind markdown --title "Build Report" --content-file report.md
cat report.md | python "$DAYBOOK_POST" --kind markdown --title "Build Report"
python "$DAYBOOK_POST" --file diagram.svg --title "Architecture Diagram"
```
The artifact URL is printed as the final stdout line.
(If you *are* working inside a Daybook source checkout, a compatibility
wrapper also exists at `scripts/daybook_post.py` in the repo root, but the
canonical helper is the one bundled in this skill.)
## Kind Cheat Sheet ## Kind Cheat Sheet
@ -94,9 +65,8 @@ presentation.
This script only **creates** artifacts — none of the following are This script only **creates** artifacts — none of the following are
possible through it. Updating an existing artifact's title/tags/metadata, possible through it. Updating an existing artifact's title/tags/metadata,
or deleting one, requires the raw API (see the instance's `docs/api.md` or or deleting one, requires the raw API (see `docs/api.md`); enabling its
`GET /api/capabilities`); enabling its public share link is feed-UI-only, public share link is feed-UI-only, with no API equivalent at all yet.
with no API equivalent at all yet.
## CDN Snippets For `html_full` ## CDN Snippets For `html_full`
@ -149,12 +119,11 @@ artifact), upload it first and reference the returned URL:
```bash ```bash
# 1. Upload -> prints the URL as the last line # 1. Upload -> prints the URL as the last line
python "$DAYBOOK_POST" --upload-image chart.png python skills/daybook-posting/scripts/daybook_post.py --upload-image chart.png
# 2. Reference that URL in your markdown, then post as usual. # 2. Reference that URL in your markdown, then post as usual
# The printed URL is absolute against your DAYBOOK_URL, e.g. # ![chart](http://127.0.0.1:8001/assets/ab12cd34ef56)
# ![chart]($DAYBOOK_URL/assets/ab12cd34ef56) python skills/daybook-posting/scripts/daybook_post.py report.md
python "$DAYBOOK_POST" report.md
``` ```
The image is linked to the artifact that references it and is deleted along with it. The image is linked to the artifact that references it and is deleted along with it.
@ -167,19 +136,19 @@ just post the file positionally; auto-mode detects `.csv`/`.json` and picks
the right kind automatically: the right kind automatically:
```bash ```bash
python "$DAYBOOK_POST" results.csv python skills/daybook-posting/scripts/daybook_post.py results.csv
python "$DAYBOOK_POST" --kind json --title "Run Summary" --content-file summary.json python skills/daybook-posting/scripts/daybook_post.py --kind json --title "Run Summary" --content-file summary.json
``` ```
`.xlsx` spreadsheets work too, with no special flag needed — just `--file` `.xlsx` spreadsheets work too, with no special flag needed — just `--file`
(or post it positionally, same as any other file): (or post it positionally, same as any other file):
```bash ```bash
python "$DAYBOOK_POST" balance_sheet.xlsx python skills/daybook-posting/scripts/daybook_post.py balance_sheet.xlsx
``` ```
The server converts it to a `csv` artifact automatically. Known The server converts it to a `csv` artifact automatically. Known
limitations (see the instance's `docs/api.md` for the full list): limitations (see `docs/api.md` for the full list):
- Only the workbook's **active sheet** converts; other sheets are dropped. - Only the workbook's **active sheet** converts; other sheets are dropped.
- Formula cells need to have been calculated and saved by real spreadsheet - Formula cells need to have been calculated and saved by real spreadsheet
@ -196,7 +165,7 @@ as a binary, use `--file path --as-markdown` to wrap it in a Markdown
fenced code block (still gets syntax highlighting): fenced code block (still gets syntax highlighting):
```bash ```bash
python "$DAYBOOK_POST" --file script.py --as-markdown --title "Generated Script" python skills/daybook-posting/scripts/daybook_post.py --file script.py --as-markdown --title "Generated Script"
``` ```
Without `--as-markdown`, it uploads as a downloadable binary file instead. Without `--as-markdown`, it uploads as a downloadable binary file instead.
@ -220,37 +189,8 @@ context like `run_id` or `model` — these end up in the artifact's
## Rules ## Rules
- Run `--check` first if unsure Daybook is reachable. - Run `--check` first if unsure Daybook is reachable.
- Make sure `DAYBOOK_URL` points at the right instance and `DAYBOOK_TOKEN` is set before posting anything real.
- Use a stable `DAYBOOK_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact). - Use a stable `DAYBOOK_SOURCE` or pass `--source` (the agent-profile, script, or person name that produced this artifact).
- Use `--tags a,b,c` for filtering. - Use `--tags a,b,c` for filtering.
- Use `--meta run_id=... --meta model=...` for machine-readable context. - Use `--meta run_id=... --meta model=...` for machine-readable context.
- Never inline multiline content into the shell command; use a file or stdin. - Never inline multiline content into the shell command; use a file or stdin.
- Add `--dry-run` to see exactly what would be sent (method, headers, payload) without actually posting — useful when unsure a payload is well-formed. - Add `--dry-run` to see exactly what would be sent (method, headers, payload) without actually posting — useful when unsure a payload is well-formed.
## `curl` Fallback (No Helper Available)
Prefer the helper above — it handles multipart, content-type inference,
JSON shaping, and provenance flags for you. Use raw `curl` **only** when you
genuinely can't run the bundled Python script (e.g. a minimal environment
with no Python). Text artifacts post as JSON to `POST /api/artifacts` with
bearer-token auth:
```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"markdown","title":"Build Report","source":"agent","content":"# Hello\n\nBody text."}'
```
File/image artifacts use multipart instead:
```bash
curl -sS -X POST "$DAYBOOK_URL/api/artifacts" \
-H "Authorization: Bearer $DAYBOOK_TOKEN" \
-F "kind=image" -F "title=Architecture Diagram" -F "source=agent" \
-F "file=@diagram.png"
```
`GET $DAYBOOK_URL/api/capabilities` (no auth needed beyond a login/token per
that instance's config) lists supported kinds, methods, and the server
version if you need to confirm the contract before posting.

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,6 @@ from django.utils import timezone
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from daybook_project import __version__
from artifacts.models import Artifact from artifacts.models import Artifact
User = get_user_model() User = get_user_model()
@ -79,42 +78,6 @@ class ArtifactAPITestCase(APITestCase):
response = self.client.get(reverse("capabilities-api")) response = self.client.get(reverse("capabilities-api"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_capabilities_advertises_skill(self):
response = self.client.get(reverse("capabilities-api"), **self.auth_headers(self.token))
self.assertEqual(response.data["methods"]["/api/skill"], ["GET"])
self.assertTrue(response.data["skill_url"].endswith("/api/skill"))
# -- skill ----------------------------------------------------------
def test_skill_requires_auth(self):
response = self.client.get(reverse("skill-api"))
self.assertEqual(response.status_code, 403)
def test_skill_returns_markdown_with_origin_filled_in(self):
response = self.client.get(reverse("skill-api"), **self.auth_headers(self.token))
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/markdown; charset=utf-8")
body = response.content.decode()
# Frontmatter is intact and the config placeholder is rewritten to this
# instance's origin (http://testserver under the test client).
self.assertIn("name: daybook-posting", body)
self.assertIn("DAYBOOK_URL=http://testserver", body)
self.assertNotIn("your-daybook-instance.example.com", body)
def test_skill_json_format(self):
response = self.client.get(
reverse("skill-api"), {"format": "json"}, **self.auth_headers(self.token)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["version"], __version__)
self.assertEqual(response.data["url"], "http://testserver")
self.assertIn("name: daybook-posting", response.data["skill"])
def test_skill_with_session(self):
self.client.force_login(self.user)
response = self.client.get(reverse("skill-api"))
self.assertEqual(response.status_code, 200)
# -- create (POST) -------------------------------------------------- # -- create (POST) --------------------------------------------------
def test_create_markdown_artifact(self): def test_create_markdown_artifact(self):