Serve the document from a Django backend, split into three tiers
Step 9. The tier split was the work; Django was the easy half.
Tier 1 — the authored scene — is the document, and it is addressed as
independently versioned leaves rather than saved whole, so one vertex drag
cannot clobber a collaborator's keying. `domain/leaf` is the document as
path -> value; `domain/wire` puts it on the wire as transit, because JSON
has neither integer map keys nor keywords and a save would quietly turn
`{0 v}` into `{"0" v}`.
Tier 2 — the dense channel blocks — is content-addressed by a hash over
every input, with the detector version inside every key through the
analysis the block descriptor names. `flow/address`'s `block-knobs` is the
invalidation table, and `address-test` does not trust it: it re-freezes the
take once per knob and asserts the biconditional, that a block's bytes
changed if and only if its key changed. That found `brow-pos` not depending
on `contour-avg` — the brow ring is smoothed, the raise is not.
Tier 3 — frames and audio — is served by the hash of its bytes out of the
same store. A manifest now names frames and carries a URL for each, so the
frame layout stopped being a shared secret between a shell script and a
ClojureScript namespace, and the `?v=` cache-buster went with it: a blob's
name is the hash of its contents, so a stale copy is not a thing that can
happen. The synthetic take's `audio.wav` moved to `static/arthur/` — an
asset the project owns, not an extraction that churns.
The server verifies rather than trusting a name it was handed: it
recomputes every key from the descriptor stored beside it, refuses an
analysis that declares no detector version, and refuses a document naming
blocks it does not hold. It hashes the descriptor TEXT, because JS prints
an integral double as `1` and Python as `1.0`, and a scheme where both ends
re-render the numbers disagrees on the first parameter that happens to be
whole.
Two loose ends from step 8 closed on the way. `pack` no longer takes a
`(track, frame)` predicate whose call sites each re-derived a feature from
an index — every track names the feature it follows, which deleted five
hand-maintained mappings. And `:dev-http` is gone: Django serves the page,
shadow-cljs only builds into the staticfiles tree.
227 CLJS tests, 31 Django tests, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 01:11:41 -04:00
|
|
|
"""The admin, which is here for one reason: tier 1 is readable.
|
|
|
|
|
|
|
|
|
|
docs/architecture.md's argument against a CRDT is partly this — "the canonical
|
|
|
|
|
document moves into an opaque blob, and every server-side thing that reads the
|
|
|
|
|
document needs it materialised back out". A leaf is transit-as-JSON in a
|
|
|
|
|
JSONField, so it is legible here, and that is a property worth being able to see.
|
|
|
|
|
"""
|
|
|
|
|
from django.contrib import admin
|
|
|
|
|
|
|
|
|
|
from .models import Analysis, Block, Blob, Clip, Footage, FootageFrame, Leaf, Project, Revision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Project)
|
|
|
|
|
class ProjectAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("name", "id", "seq", "updated")
|
|
|
|
|
search_fields = ("name", "id")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Clip)
|
|
|
|
|
class ClipAdmin(admin.ModelAdmin):
|
2026-10-03 01:24:45 -04:00
|
|
|
list_display = ("cid", "project", "name")
|
Serve the document from a Django backend, split into three tiers
Step 9. The tier split was the work; Django was the easy half.
Tier 1 — the authored scene — is the document, and it is addressed as
independently versioned leaves rather than saved whole, so one vertex drag
cannot clobber a collaborator's keying. `domain/leaf` is the document as
path -> value; `domain/wire` puts it on the wire as transit, because JSON
has neither integer map keys nor keywords and a save would quietly turn
`{0 v}` into `{"0" v}`.
Tier 2 — the dense channel blocks — is content-addressed by a hash over
every input, with the detector version inside every key through the
analysis the block descriptor names. `flow/address`'s `block-knobs` is the
invalidation table, and `address-test` does not trust it: it re-freezes the
take once per knob and asserts the biconditional, that a block's bytes
changed if and only if its key changed. That found `brow-pos` not depending
on `contour-avg` — the brow ring is smoothed, the raise is not.
Tier 3 — frames and audio — is served by the hash of its bytes out of the
same store. A manifest now names frames and carries a URL for each, so the
frame layout stopped being a shared secret between a shell script and a
ClojureScript namespace, and the `?v=` cache-buster went with it: a blob's
name is the hash of its contents, so a stale copy is not a thing that can
happen. The synthetic take's `audio.wav` moved to `static/arthur/` — an
asset the project owns, not an extraction that churns.
The server verifies rather than trusting a name it was handed: it
recomputes every key from the descriptor stored beside it, refuses an
analysis that declares no detector version, and refuses a document naming
blocks it does not hold. It hashes the descriptor TEXT, because JS prints
an integral double as `1` and Python as `1.0`, and a scheme where both ends
re-render the numbers disagrees on the first parameter that happens to be
whole.
Two loose ends from step 8 closed on the way. `pack` no longer takes a
`(track, frame)` predicate whose call sites each re-derived a feature from
an index — every track names the feature it follows, which deleted five
hand-maintained mappings. And `:dev-http` is gone: Django serves the page,
shadow-cljs only builds into the staticfiles tree.
227 CLJS tests, 31 Django tests, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 01:11:41 -04:00
|
|
|
list_filter = ("project",)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Leaf)
|
|
|
|
|
class LeafAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("path", "project", "version", "updated")
|
|
|
|
|
list_filter = ("project",)
|
|
|
|
|
search_fields = ("path",)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Revision)
|
|
|
|
|
class RevisionAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("project", "seq", "summary", "author", "created")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Footage)
|
|
|
|
|
class FootageAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("label", "source", "fps", "frames", "width", "height", "created")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(FootageFrame)
|
|
|
|
|
class FootageFrameAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("footage", "index", "blob")
|
|
|
|
|
list_filter = ("footage",)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Analysis)
|
|
|
|
|
class AnalysisAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("key", "detector", "version", "footage", "created")
|
|
|
|
|
search_fields = ("key", "detector", "version")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Block)
|
|
|
|
|
class BlockAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("key", "role", "analysis", "data", "state", "created")
|
|
|
|
|
list_filter = ("role",)
|
|
|
|
|
search_fields = ("key",)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@admin.register(Blob)
|
|
|
|
|
class BlobAdmin(admin.ModelAdmin):
|
|
|
|
|
list_display = ("digest", "media_type", "size", "created")
|