diff --git a/config/settings/base.py b/config/settings/base.py
index 5fb9172..8a681f1 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -29,6 +29,8 @@ INSTALLED_APPS = [
"intent.orgs",
"intent.projects",
"intent.features",
+ "intent.files",
+ "intent.implementations",
"intent.issues",
"intent.comments",
]
@@ -78,6 +80,9 @@ DATABASES = {
}
}
+# Uploads up to 25 MB (see intent.files). Larger bodies spill to a temp file, not memory.
+FILE_UPLOAD_MAX_MEMORY_SIZE = 2_621_440
+
# Rate limits (allauth's and ours) live here, so the cache must be shared by every worker
# and survive restarts. The database is the only shared store on a one-machine deploy.
CACHES = {
diff --git a/config/urls.py b/config/urls.py
index 7d748d0..c50df45 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -31,7 +31,15 @@ urlpatterns = [
path("/new/", project_create, name="project_create"),
path("/", include("intent.orgs.urls")),
path("//", include("intent.projects.urls")),
+ path(
+ "//features//files/",
+ include("intent.files.urls"),
+ ),
path("//features/", include("intent.features.urls")),
path("//issues/", include("intent.issues.urls")),
+ path(
+ "//implementations/",
+ include("intent.implementations.urls"),
+ ),
path("//", include("intent.comments.urls")),
]
diff --git a/docs/plan.md b/docs/plan.md
index 63d25b6..f662b42 100644
--- a/docs/plan.md
+++ b/docs/plan.md
@@ -26,11 +26,12 @@ Decisions made in the concept are not repeated. These fill the gaps.
| Issue → feature | From an issue, a collaborator follows "Create a feature from #N" or "Update INT-3 for #N". The feature form opens prefilled (title and description from the issue, or a change note naming it), ties the new revision to the issue, links the feature, and offers a checked "Mark #N accepted" box. Accept and Reject buttons also work on their own. |
| Feature statuses | `active`, `withdrawn`, `superseded` (requires `superseded_by` feature). |
| Implementation status | `active`, `archived`. |
-| Feature × implementation status | `n/a`, `not_started`, `in_progress`, `shipped`, `retired`; stamped with the feature revision it applies to. Missing row displays as `not_started @ —`. If stamped revision < current revision, display "behind". |
+| Feature × implementation status | `n/a`, `not_started`, `in_progress`, `shipped`, `retired`; stamped with the feature revision it applies to. Missing row displays as `not_started @ —`. If stamped revision < current revision, display "behind". Each implementation page is one form: a status select per feature, plus a "Still … at revision N" checkbox on rows that are behind, which re-stamps without changing the status. Only changed or confirmed rows are written. Implementation links must be http(s). Collaborators create, edit and archive implementations; project owners delete them (typed name to confirm). |
| Hierarchy | Single optional parent within the same project. Cycles rejected. Moving a feature records a revision. Statuses independent in V1. |
-| Files | Belong to a feature, typed `prototype` or `documentation`. Replacing a file creates a new file version and a feature revision; old versions stay (so old image-pin threads still resolve). Size cap 25 MB; allowed types: images, PDF, markdown/text, HTML, zip. |
+| Files | Belong to a feature, typed `prototype` or `documentation`. Adding, replacing or removing a file is a feature revision with a required change note; the revision records what happened ("Added prototype “a.png”") and shows it in history. Replacing creates a new file version; old versions stay (so old image-pin threads still resolve). Removed files stay too, so old revisions list the files they had. Size cap 25 MB; allowed types: images (png, jpg, gif, webp, avif, svg), PDF, Markdown/text, HTML, zip. The content type comes from the extension, never the browser. Stored under opaque paths (`files//`). Markdown files render on their page; text shows as text; PDFs and zips open or download. |
| Line-anchored threads | Anchor = feature revision + start/end source line of one top-level Markdown block (paragraph, list, heading, code block). Each block has its own `` comment form, so it works without JS. A thread goes "outdated" only when the description text has changed since its revision (a status-only revision doesn't outdate it); outdated threads move to "Comments on earlier revisions", quote their original lines, and link to the revision. No attempt to re-map lines. A line comment posted against a stale revision is refused and the draft is shown back. |
-| Image threads | Anchor = file version + x/y as fractions (0–1). |
+| Image threads | Anchor = file version + x/y as fractions (0–1). Pins are drawn as an SVG overlay using percentage coordinates (no inline styles), numbered per version, and each is a link to its thread. Placing a pin: two number fields (across %, down %) work without JS; clicking the image fills them in. New comments go on the latest version only; threads on other versions are listed separately with a link to that version. |
+| File threads | Anchor = file version, like image threads without a point. |
| Comments | Edits keep history (visible "edited" link to a history page). Author or project owner may delete; a deleted comment shows as a tombstone and its text and history are removed. Delete asks for confirmation in a disclosure, so it works without JS. Resolving a feature thread is allowed for the thread author or any collaborator; resolved threads start collapsed. An issue has exactly one thread, its discussion, which isn't resolvable (the issue status says that). |
| Thread → issue | Original commenter (while they can contribute) or any project collaborator. Thread gets `promoted_to` issue and is resolved; the issue is linked to the thread's feature, its body is prefilled with the quoted lines and comments, and its page links back to the thread. |
| Deleted users | Content remains, attributed to a "ghost" user. |
@@ -51,9 +52,9 @@ Decisions made in the concept are not repeated. These fill the gaps.
| Sysadmins | Django `is_staff` users with passwords, Django admin at `/admin/`. A model constraint rejects staff users as org members. Regular users have no password (unusable). Only superusers can grant staff or superuser status or permissions in the admin. |
| Cache | Django's database cache (`createcachetable` runs at start). It holds allauth's rate limits and the invite throttle, so it must be shared by all gunicorn workers and survive restarts; the database is the only shared store on a one-machine deploy. Dev and tests use the in-memory cache. |
| Database | SQLite on a Fly volume. `transaction_mode = "IMMEDIATE"`, WAL mode, `busy_timeout` 5 s. Litestream replicates to B2 continuously. |
-| Files | Tigris via `django-storages` S3 backend, private bucket, time-limited signed URLs. HTML prototypes are shown in a sandboxed `