The auth matrix probes documented endpoints with their documented form
params; the faction param was declared with location body instead of
form, so the anonymous probe sent an empty body and hit 422
validation before the auth guard. And the first fight always triggers
a lead-change event that outranks the fight event at the top of the
ticker, so the e2e flow now asserts on the ticker as a whole.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new post attachment type beside polls: the composer gains a Start
Opinion War builder (same disabled-inputs opt-in as the poll builder)
that names exactly two factions; the battle runs for exactly 7 days
from post creation. Members join a side, may defect at any time
(damage already dealt stays with the faction it was dealt to), and
fight once per 24 hours per battle. A fight spends 25 Code Farm coins
and deals deterministic level-weighted damage: 100 + 10 * min(level,
20) HP, so a newcomer deals 110 and a veteran caps at 300 - no
randomness anywhere.
The battle renders on the post card as a CSS pixel-art battlefield
(box-shadow sprites: castles, faction flags, marching soldiers, a
flickering campfire; steps() animation, disabled under reduced motion)
with live HP bars, a countdown, the viewer's faction strip, top
contributors and an event ticker. Live frames ride pub/sub on
public.battle.{uid} via a relay on the service-lock owner, with the
durable opinion_war_events trail (per-war atomic seq) as the source of
truth and a 15s incremental poller as fallback. /battles lists battles
with active/ended/mine filters, search and pagination.
Every mutation is a conditional UPDATE via conditional_update_row: the
fight sequence claims the cooldown first, then spends coins, then lands
the damage, compensating earlier steps on any later refusal so a crash
costs a turn, never coins. Resolution is lazy on read (no cron):
an exactly-once CAS computes the winner in the statement, awards XP
(participation, winner bonus, top damage dealer bonus; draws pay
participation only), emits the result event and notifies fighters. The
OpinionWarService backstop resolves unviewed wars and sends
fight-ready notifications, exactly-once via a marker CAS.
Fan-out: battle notification type, four badges, audit keys
(battle.create/join/switch/fight/resolve), Devii actions (join/fight
confirm-gated), API docs group, docs prose page, sitemap and topnav
entries, REPORTABLE_TARGETS registration, post-delete cascades,
README and nested CLAUDE.md documentation.
Verified with the four-layer procedure: property checks over the full
damage domain, 1200-step stateful fuzz (hp-sum invariant, coins never
negative, resolved totals frozen), and real 8-process races proving
exactly-once semantics for concurrent fights, double-spends across two
wars, resolution XP and double-joins. Persisted tests in
tests/unit/services/opinionwar, tests/api/battles, tests/e2e/battles
and tests/api/posts/create.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_attachment_display.html iterates a context variable named
`attachments`, so every caller binds it before the include.
_post_card.html was the one caller that did not: it guarded on
item.attachments but included the partial with nothing bound, so the
gallery looped over whatever `attachments` happened to be in the
surrounding page context and rendered empty. Every post with an image
looked image-less on the feed and on profiles, and on a project page -
where project_detail.html sets `attachments` at template scope for the
project's own files - a devlog card would have rendered the project's
files as its own.
With the image actually reaching the card, render a lone one properly:
a gallery holding exactly one item gets a `single` class and takes the
full content column (max-height 480px, object-fit contain, no hover
scale), matching the original DevPlace. That branch serves the stored
original rather than thumbnail_url, because a thumbnail is 200px on its
longest side and stretching it to the column width is visibly blurry.
Animated GIFs needed no change and now have a test proving it: they
never had a thumbnail to flatten, so they already took the original-file
path and simply render larger.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressing Ctrl+V with a screenshot on the clipboard now attaches it
immediately instead of requiring a trip through the file picker.
The clipboard reader lives in dp-upload behind a new opt-in `paste`
boolean attribute: with it set, the component binds one paste listener
on its closest form and routes the clipboard image files through the
same handleFiles path as the picker and the drop target, so validation,
limits, the terms gate and the hidden attachment_uids field are shared.
A paste carrying plain text is never swallowed.
It is opt-in rather than a form-wide default because a form may hold
several upload buttons - projects.html has cover and logo beside the
attachment one - and a default would attach one pasted screenshot to
all of them.
Set on _attachment_form.html, so every form including it inherits the
behaviour (post composer, post edit, gists, projects, issues,
screenshots), plus _comment_form.html, messages.html and the embed-mode
skeleton AppChat builds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cover and logo widgets declared allowed-types as bare extensions
(png,jpg,jpeg,gif,webp), but dp-upload builds the candidate extension
with a leading dot before testing membership, so every selected file
was refused with "type is not allowed". The four widgets were the only
hardcoded lists in the codebase: every other call site passes
allowed_file_types(), which defaults to empty and therefore disables
the client filter entirely, which is why nothing else exposed the
mismatch.
Rather than dotting a duplicated literal in four places, the effective
list now comes from a new allowed_image_types() Jinja global that
intersects allowed_extensions() with IMAGE_EXTENSIONS. That reuses the
one server-side choke point, so the widget can never advertise a type
the upload gate would reject, and narrowing the admin allowed_file_types
setting narrows these widgets with it. IMAGE_EXTENSIONS rather than
POST_IMAGE_EXTENSIONS is the correct set here because the route guard
is _hero_attachment_uid, which accepts any is_image attachment, and
bmp/tiff both upload and pass it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The project detail page becomes a full project showcase built entirely
from existing platform mechanisms. One encompassing dark card wraps the
page; inner panels (tab bar, sidebar cards, devlog entries, comments)
sit one elevation lighter. The hero opens with a cover banner and an
optional logo tile, both plain attachment references
(cover_attachment_uid/logo_attachment_uid) uploaded through the
standard dp-upload attachment widget and linked via the existing
link_attachments choke point - the route validates each uid belongs to
the actor and is an image, and an empty value on edit keeps the current
one. The title block, type/platform chips and author row overlay the
banner behind a scrim with a dark text shadow, next to an owner-set
Visit Website CTA; website_url and repo_url are normalized in models
and render with rel noopener nofollow.
An anchor tab bar (Overview, Devlog, Screenshots when present,
Comments, Files) navigates the page. The main column keeps About, the
devlog timeline (with devlog_count and an owner Post update button
opening the shared composer preset to the devlog topic + project - the
form now lives once in _post_composer_form.html, included by feed.html
and project_detail.html), a Screenshots gallery built from image
attachments minus the cover/logo (thumbnails, lightbox, 12 rendered),
and the comment thread; the sidebar holds Links, Stats and the Author
card. Owners add gallery images from the More menu via
POST /projects/{slug}/screenshots (owner-only, audit
project.screenshots.add, Devii action project_add_screenshots, docs id
projects-screenshots). comment_count/devlog_count ride
ProjectDetailOut, the new fields ride ProjectOut, and the create/edit
faces (modals, Devii actions, API docs) carry them. The project
comment/files e2e tests scope their locators per the documented
dual-control idiom, and new unit/api/e2e tests cover URL normalization,
the counts, the hero attachment guard, the screenshots flow and the
preset composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reference design uses flat primary buttons, so every interactive
control drops the brand gradient and the accent glow: .btn-primary
(flat accent, accent-hover on hover), the feed nav tabs, the projects
tabs, the sidebar active link, and the create-post FAB (which keeps a
neutral elevation shadow). Gradients remain only on decorative meters
and stripes (XP fills, progress bars, the project card top stripe).
The now-unused --glow-accent token is removed, and deepsearch.css
loses its var() fallback on the progress gradient.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The signed-in dashboard is a 1200px two-column grid, but the shared
Code Farm promo section stayed at the guest page's 960px centered
measure, so it floated misaligned under the dashboard columns. A
landing-game-dashboard modifier (applied only when a user is signed
in) stretches it to the dashboard's own measure and gutters. The
Latest Posts grid becomes repeat(auto-fit, minmax(320px, 1fr)) so a
single post fills the row instead of leaving a dead half-column while
two or more still lay out as the original two columns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reference design is the devplace Next.js main branch, not the
terminal branch the previous commit tracked. Swap the token values to
that design language: deep indigo page (#271b5b) over near-black indigo
cards (#13112a/#1a1736), #11101f hairlines, purple-tinted text ramp
(#e9e4ff/#c8c3e5/#9b93c9), burnt-orange primary accent (#b73f1e) with
a raspberry hover (#af3050) and a rust-to-plum brand gradient, white
on-accent text, soft rounded radii (8/12/16px), the reference's diffuse
soft/medium/strong shadows, a warm accent glow, and Trebuchet MS as the
UI font (system font - the vendored JetBrains Mono files are removed
along with their base.html link).
Follow-through updated to match: theme-color meta + PWA manifest
(#271b5b), offline page, avatar fallback SVG, statistics chart palette
(brand ramp), chat light-theme overrides, OG image + app icons
regenerated in the new palette, and the /docs/styles-colors guide.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retheme every token in variables.css to the terminal design language:
near-black surfaces (#0a0a0f/#111118/#1a1a24), terminal green accent
(#22c55e) with dark on-accent text, gray text ramp, #222233 borders,
sharp 4-8px radii, neutral depth shadows, and JetBrains Mono as the
primary UI font (vendored latin woff2, no CDN).
Follow-through outside the token file: white-on-accent buttons/tabs now
use the new --on-accent token for contrast, three var() fallback
violations shipping the old palette are gone, the dead --bg-hover token
is replaced with --bg-card-hover, avatar fallback SVG, statistics chart
palette, chat light-theme overrides, offline page, PWA manifest,
theme-color meta, OG image + app icons regenerated in the new palette,
and the /docs/styles-colors style guide now documents the new values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>