diff --git a/CHANGELOG.md b/CHANGELOG.md index 26f8719..60b5f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,6 @@ former name, "Daybook".) _Nothing yet._ -## [0.3.2] — 2026-09-04 - -### Added -- `GET /api/artifacts/{id}/raw` — the artifact's full content, separate from the - metadata detail endpoint. Bearer-token (or session) auth, owner-scoped, so an - agent can read back and verify exactly what it posted: exact source text for - `markdown`/`html_full`/`json`/`csv`, file bytes for `image`/`pdf`/`binary`, - `404` for render-only `html_fragment`/`deck`. The metadata response now - includes `api_raw_url`, and `/api/capabilities` advertises the route. - ## [0.3.1] — 2026-09-04 ### Changed @@ -138,8 +128,7 @@ First tagged release. - 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.3.2...HEAD -[0.3.2]: https://git.cygnus.systems/peter/daybook/compare/v0.3.1...v0.3.2 +[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.3.1...HEAD [0.3.1]: https://git.cygnus.systems/peter/daybook/compare/v0.3.0...v0.3.1 [0.3.0]: https://git.cygnus.systems/peter/daybook/compare/v0.2.1...v0.3.0 [0.2.1]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...v0.2.1 diff --git a/artifacts/urls.py b/artifacts/urls.py index 008ceb5..1cbd657 100644 --- a/artifacts/urls.py +++ b/artifacts/urls.py @@ -19,7 +19,6 @@ urlpatterns = [ path("api/keys/", views.ApiKeyDetailView.as_view(), name="api-key-detail"), path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"), path("api/artifacts/", views.ArtifactDetailView.as_view(), name="artifact-detail-api"), - path("api/artifacts//raw", views.ArtifactRawView.as_view(), name="artifact-raw-api"), path("settings/", views.settings_home, name="settings-home"), path("settings/account/", views.settings_account, name="settings-account"), path("settings/api-keys/", views.settings_api_keys, name="settings-api-keys"), diff --git a/artifacts/views.py b/artifacts/views.py index d82eed9..99f326b 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -303,37 +303,6 @@ class ArtifactDetailView(APIView): return Response(status=204) -class ArtifactRawView(APIView): - """The artifact's full content, for read-back / verification. Same auth as - the detail endpoint (bearer token or session), owner-scoped. - - GET /api/artifacts/{id}/raw - - markdown / html_full / json / csv -> exact posted source (text/plain) - - image / pdf / binary -> the stored file bytes - - html_fragment / deck -> 404 (render-only; no raw kept) - """ - - def get(self, request, artifact_id): - user = resolve_request_user(request) - if user is None: - return Response({"detail": "Authentication required (session or bearer token)."}, status=403) - artifact = get_object_or_404(Artifact, pk=artifact_id, owner=user) - - raw_path = raw_artifact_path(artifact) - if raw_path and raw_path.exists(): - return FileResponse(raw_path.open("rb"), content_type="text/plain; charset=utf-8") - - if artifact.kind in {Artifact.IMAGE, Artifact.PDF, Artifact.BINARY}: - path = safe_artifact_path(artifact.path) - if path.exists(): - content_type, _ = mimetypes.guess_type(path.name) - return FileResponse(path.open("rb"), content_type=content_type or "application/octet-stream") - - return Response( - {"detail": "No raw content available for this kind (render-only)."}, status=404 - ) - - class CapabilitiesView(APIView): """Static capability info. Reachable by a logged-in session or a valid API token, so agents can check connectivity before posting. Exposes no user data.""" @@ -345,7 +314,6 @@ class CapabilitiesView(APIView): methods = { "/api/artifacts": ["GET", "POST"], "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], - "/api/artifacts/{id}/raw": ["GET"], "/api/assets": ["POST"], "/api/capabilities": ["GET"], "/api/skill": ["GET"], @@ -1306,7 +1274,6 @@ def serialize_artifact(artifact, request): "url": request.build_absolute_uri(reverse("artifact", args=[artifact.id])), "api_url": request.build_absolute_uri(reverse("artifact-detail-api", args=[artifact.id])), "raw_url": request.build_absolute_uri(f"{reverse('artifact', args=[artifact.id])}?raw=1"), - "api_raw_url": request.build_absolute_uri(reverse("artifact-raw-api", args=[artifact.id])), "raw_available": raw_artifact_path(artifact) is not None, "source": artifact.source, "title": artifact.title, diff --git a/daybook_project/__init__.py b/daybook_project/__init__.py index 47770b1..72aff5b 100644 --- a/daybook_project/__init__.py +++ b/daybook_project/__init__.py @@ -1,4 +1,4 @@ # 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 # GET /api/capabilities ("version"). -__version__ = "0.3.2" +__version__ = "0.3.1" diff --git a/docs/api.md b/docs/api.md index 187270c..e804074 100644 --- a/docs/api.md +++ b/docs/api.md @@ -465,28 +465,26 @@ curl -X DELETE "http://127.0.0.1:8001/api/artifacts/a1b2c3d4" \ ## Rendered And Raw Content -**For agents (bearer token):** fetch the full content from the API — separate -from the metadata `GET /api/artifacts/{id}`: +Rendered browser view: ```http -GET /api/artifacts/{id}/raw +GET /a/{id} ``` -Same auth as the detail endpoint (bearer token or session), owner-scoped, so an -agent can read back and verify exactly what it posted. The metadata response -includes `api_raw_url` pointing here. +Raw/source view: -- `markdown` / `html_full` / `json` / `csv` → the exact posted source, as - `text/plain`. -- `image` / `pdf` / `binary` → the stored file bytes (with its content type). -- `html_fragment` / `deck` → `404` (render-only; no raw source is kept). +```http +GET /a/{id}?raw=1 +``` -Byte-faithful to what's stored; note the deploy (Linux) preserves `\n` line -endings. +Raw currently exists for: -**For browsers (session):** the human viewer at `GET /a/{id}` (rendered) and -`GET /a/{id}?raw=1` (source) is session-authed; use the API `…/raw` endpoint -above for token access. +| Kind | Raw content | +|---|---| +| `markdown` | Preserved `.md` source. | +| `html_full` | Stored HTML file served as text. | +| `json` | Preserved original JSON text. | +| `csv` | Preserved original CSV text. | If raw content is unavailable, AgentFeed returns a small HTML fallback page. diff --git a/tests/api/test_artifacts.py b/tests/api/test_artifacts.py index 0c8797c..34193b7 100644 --- a/tests/api/test_artifacts.py +++ b/tests/api/test_artifacts.py @@ -253,46 +253,6 @@ class ArtifactAPITestCase(APITestCase): response = self.client.get(reverse("artifact-detail-api", args=[artifact_id])) self.assertEqual(response.status_code, 403) - # -- raw content (GET {id}/raw) --------------------------------------- - - def _raw_body(self, response): - return b"".join(response.streaming_content).decode() - - def test_raw_returns_exact_content_with_bearer(self): - create = self.create_artifact(content="# Exact\n\nverify me") - artifact_id = create.data["id"] - # metadata detail advertises the raw endpoint - self.assertTrue(create.data["api_raw_url"].endswith(f"/api/artifacts/{artifact_id}/raw")) - - response = self.client.get( - reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.token) - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response["Content-Type"], "text/plain; charset=utf-8") - # Content round-trips faithfully; normalize newlines since the stored - # sidecar is written in the host's text mode (CRLF on Windows dev, LF on - # the Linux deploy). - self.assertEqual(self._raw_body(response).replace("\r\n", "\n"), "# Exact\n\nverify me") - - def test_raw_requires_auth(self): - artifact_id = self.create_artifact().data["id"] - response = self.client.get(reverse("artifact-raw-api", args=[artifact_id])) - self.assertEqual(response.status_code, 403) - - def test_raw_owner_scoped(self): - artifact_id = self.create_artifact().data["id"] - response = self.client.get( - reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.other_token) - ) - self.assertEqual(response.status_code, 404) - - def test_raw_not_available_for_render_only_kind(self): - artifact_id = self.create_artifact(kind="html_fragment", content="

hi

").data["id"] - response = self.client.get( - reverse("artifact-raw-api", args=[artifact_id]), **self.auth_headers(self.token) - ) - self.assertEqual(response.status_code, 404) - def test_patch_updates_title_and_tags(self): artifact_id = self.create_artifact().data["id"] response = self.client.patch(