<feed xmlns='http://www.w3.org/2005/Atom'>
<title>cccms/app/models/node.rb, branch erdgeist-modernize-auth</title>
<subtitle>[no description]</subtitle>
<id>https://erdgeist.org/gitweb/cccms/atom?h=erdgeist-modernize-auth</id>
<link rel='self' href='https://erdgeist.org/gitweb/cccms/atom?h=erdgeist-modernize-auth'/>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/'/>
<updated>2026-07-16T13:12:07Z</updated>
<entry>
<title>Split current_unique_name in a setter and an accessor</title>
<updated>2026-07-16T13:12:07Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-16T13:12:07Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=b701669d0d9a5d7be01f99b8e725c05c6c5d52ce'/>
<id>urn:sha1:b701669d0d9a5d7be01f99b8e725c05c6c5d52ce</id>
<content type='text'>
Also clean up some instance variable assignments on the way.
</content>
</entry>
<entry>
<title>Record the full lifecycle contract in NodeAction entries</title>
<updated>2026-07-16T00:40:03Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-16T00:40:03Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=c0c9250141c2fa64fed967b8b9ad9bada3694c3d'/>
<id>urn:sha1:c0c9250141c2fa64fed967b8b9ad9bada3694c3d</id>
<content type='text'>
A contract comment above NodeAction.record! now specifies every
verb's metadata shape. NodeAction.head_diff computes the publish
diff between an outgoing head and its replacement -- default-locale
title pair always, author/tags pairs and template/assets/abstract/
body flags only when changed, and a per-locale translation_diff
with added/removed/changed status. It is a pure function of its two
pages, shared by publish, rollback, and the future backfill, and
reads translation rows directly so fallbacks never masquerade as
content.

publish entries carry via ("draft" or "revision"); restore_revision!
is now transactional, takes the acting user, and logs through the
same diff. Staged slug/parent changes applied at publish log a move
entry with the path pair. Node creation logs a create entry with
initial title and path. The draft-scoped translation_destroy writer
is retired -- locale removal is recorded by the publish diff, where
it becomes public fact.
</content>
</entry>
<entry>
<title>Add NodeAction: an append-only log of who did what to a node</title>
<updated>2026-07-15T02:23:09Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-14T23:41:34Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=d56155231814633e04f856d22646fea24ef97011'/>
<id>urn:sha1:d56155231814633e04f856d22646fea24ef97011</id>
<content type='text'>
node_id/page_id/user_id are lookup and ordering only -- all three
nullify on delete, so an entry outlives its actor and its subject.
Everything that must survive those deletions lives in a mandatory
metadata jsonb written once at creation: the actor's username, the
node's human-readable name (pinned to the default locale), and
action-specific extras such as publish's title from/to.

NodeAction.record! is the single constructor, so every entry gets
the same baseline metadata without each call site re-implementing
it. occurred_at is one field for live and backfilled entries alike;
inferred_from distinguishes them -- nil means witnessed at the
moment it happened, populated names how a backfilled entry was
estimated.

Instrumented so far: publish (crediting the actual publisher,
threaded through from the controller -- previously nobody had the
act of publishing recorded anywhere), revert's discard_autosave and
destroy_draft branches, and translation destroy. publish_draft! now
runs in a transaction so the promotion and its log entry land
together. The remaining verbs follow once this mechanism has proven
itself.
</content>
</entry>
<entry>
<title>Retire wipe_draft! and find_or_create_draft from production code</title>
<updated>2026-07-14T12:31:18Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-14T12:31:18Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=cca0e57b21d506cd341b927984bcb68f0e461783'/>
<id>urn:sha1:cca0e57b21d506cd341b927984bcb68f0e461783</id>
<content type='text'>
Both had already lost their reason to exist as production API:
wipe_draft!'s one remaining callsite (nodes#show) was removed two
sessions ago, and find_or_create_draft had zero production callers
left at all -- confirmed by a fresh grep, not assumed -- every one of
its ~65 call sites was test setup, unrelated to what those tests
actually cover.

wipe_draft! is deleted outright, along with its two tests -- the
lock/draft/autosave cleanup it silently performed already has
explicit, always-visible manual equivalents (Unlock, Discard
Autosave, Destroy Draft), so nothing real is lost.

find_or_create_draft moves to test_helper.rb as a plain method on
ActiveSupport::TestCase, alongside the create_node_with_draft/
create_node_with_published_page helpers already living there --
extending the framework's own designated test-extension point
rather than reopening the Node model from test code. Its three
tests of real dispatch behavior (idempotency on repeat calls, and
raising when a second user contends for the lock) are kept, since
~65 other tests depend on this helper actually working correctly;
only the call syntax changed, from node.find_or_create_draft(user)
to find_or_create_draft(node, user).
</content>
</entry>
<entry>
<title>wipe_draft!: add a grace period to the lock-with-no-draft branch</title>
<updated>2026-07-13T14:36:06Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-13T14:36:06Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=e2705119ba718d40ff4f29c00027c2dfecef01ce'/>
<id>urn:sha1:e2705119ba718d40ff4f29c00027c2dfecef01ce</id>
<content type='text'>
Its other two branches both require a day's inactivity before
touching anything; this one didn't, so a lock acquired seconds ago
with nothing drafted yet looked identical to one abandoned for a
week -- confirmed with a throwaway script showing a lock cleared
inside a single method call. Guarded on autosave.nil? specifically,
since the method's first line already guarantees any autosave
reaching this branch is a day old by construction.
</content>
</entry>
<entry>
<title>Carry over other-locale translations when creating an autosave</title>
<updated>2026-07-13T12:12:33Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-13T12:12:33Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=ea8ee1d35948c6adc4a377c9eb77d99b2d856932'/>
<id>urn:sha1:ea8ee1d35948c6adc4a377c9eb77d99b2d856932</id>
<content type='text'>
Node#autosave! only copied assets forward onto a newly created
autosave, not translations. Invisible for single-locale nodes, but a
node with an existing multi-locale head/draft that gets locked and
edited in one locale would silently drop every other locale's
translation from the new autosave -- and from there into the draft
and eventually head, since save_draft! faithfully clones whatever
the autosave actually holds.

Fixed by using clone_attributes_from, which already does a complete
clone (translations, assets, tags, template, published_at) -- the
same method save_draft! already relies on two branches later, so no
new mechanism, just closing the one place still doing a partial,
hand-rolled version of it.
</content>
</entry>
<entry>
<title>Asset picker: attach/detach/reorder UI, read-only view, autosave fix</title>
<updated>2026-07-13T02:54:33Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-13T02:54:33Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=8baac265059b70da0148487458ee4077b15f155e'/>
<id>urn:sha1:8baac265059b70da0148487458ee4077b15f155e</id>
<content type='text'>
Replaces nodes#edit's old Images section -- a hidden panel dumping
every image asset in the system unfiltered (#image_browser) plus a
raw drag-and-drop box (#image_box) -- with a small search-and-click
picker built on the endpoint from the last two commits. Attaching
posts immediately and appends the new thumbnail via a cloned
&lt;template&gt; -- icons only render correctly through the Rails helper
server-side, so the template holds real, pre-rendered markup for JS
to clone rather than duplicating raw SVG in a JS string. Reordering is
jQuery UI sortable on the small attached list only, with a dedicated
drag handle rather than the whole thumbnail.

Two bugs caught while click-testing, fixed here rather than shipped
and patched after: the search panel never closed after attaching an
image, since the success handler re-triggered focus to keep it open
for attaching several in a row -- which meant it just re-populated
itself forever instead of signaling "done." Fixed to close explicitly;
a click-outside-closes handler was added alongside it, matching the
affordance the top-bar search already has.

A real, independent, pre-existing data bug surfaced during the same
testing: Node#autosave!'s first-time-creation branch never carried
related assets forward from whatever page was previously current --
attach an image, let autosave fire once, and it silently landed on a
fresh, assetless Page row. Long-dormant, not introduced by this work,
just finally exercised by something that made it visible. Fixed inside
the `unless self.autosave` guard specifically -- running this on every
call, not just creation, would overwrite anything attached directly to
an existing autosave in between, a worse bug than the one being fixed.

nodes#show gains a read-only Images section, rendered only when a page
actually has attached images, so an attachment can be confirmed
present without entering the edit/lock cycle -- useful on its own, and
specifically useful the next time an asset bug needs investigating.
Its thumbnail CSS is shared with the edit view's picker via a class
(.thumbnail_list) rather than duplicated under a second name.
</content>
</entry>
<entry>
<title>Add RelatedAssetsController: search, attach, detach, reorder</title>
<updated>2026-07-13T01:22:37Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-13T01:22:37Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=70653b681d10917b77dced08f577446ced7568f1'/>
<id>urn:sha1:70653b681d10917b77dced08f577446ced7568f1</id>
<content type='text'>
Backend for the asset-picker rebuild -- replaces the plan to dump
every image asset in the system into a hidden, unfiltered browse
panel on every node edit (the actual current behavior, confirmed by
reading nodes/edit.html.erb directly) with a small, name-scoped search
endpoint plus create/destroy/update for attach, detach, and reorder.

No schema change needed -- RelatedAsset already had everything this
requires (asset_id, page_id, an acts_as_list position). search excludes
assets already attached to the page, keeping results meaningfully small
given hundreds of assets total but only a handful per node in practice.
create is find_or_create_by! rather than a bare create!, guarding
against the same asset being attached twice from two separate search
results. update leans on acts_as_list's own insert_at rather than
custom position-shifting logic.

Node#editable_page (autosave || draft || head) is extracted since this
is now its third call site with identical logic -- deliberately not
touching nodes#show, which resolves draft || head without autosave on
purpose, a different and correct semantic for "current committed
state" versus "what's actively being edited."
</content>
</entry>
<entry>
<title>Rebuild the admin dashboard: icon nav, search, signposts, widgets</title>
<updated>2026-07-12T21:42:43Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-12T21:42:43Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=b271648f89cba7cafafa1b73b1658b1c1bc0e4b0'/>
<id>urn:sha1:b271648f89cba7cafafa1b73b1658b1c1bc0e4b0</id>
<content type='text'>
Replaces the old admin#index wizard -- accreted over years, never
designed as a whole -- with the dashboard settled on this session:
a three-icon nav (dashboard/search/log out, no locale selector), a
nodes-first search bar, four task signposts, and two symmetric
widgets (drafts/autosaves, recent changes) with a quiet housekeeping
row beneath them.

Node.recently_changed now filters and orders by the head page's own
updated_at instead of the node's blanket timestamp, so a lock/unlock
cycle with no actual publish no longer surfaces here, and the
original publisher is no longer misattributed to someone else's
housekeeping action. This also restores the "published" qualifier on
each entry, which the query previously couldn't guarantee was true.

@mynodes and its dedicated "My Work" table are retired along with the
old wizard -- "Continue my work" is a link to the existing, already-
correct NodesController#mine instead of a second, duplicate query.
Its one dedicated test (dedup across multiple revisions by the same
user) is ported to nodes_controller_test.rb, since mine already
carries the same .distinct protection the old query did; it just had
no test of its own until now.
</content>
</entry>
<entry>
<title>Extract drafts/recent query logic from controller into Node</title>
<updated>2026-07-12T12:26:30Z</updated>
<author>
<name>erdgeist</name>
<email>erdgeist@erdgeist.org</email>
</author>
<published>2026-07-12T12:26:30Z</published>
<link rel='alternate' type='text/html' href='https://erdgeist.org/gitweb/cccms/commit/?id=5803c59192b7fb05840d0b452eb64d9f997f3d8f'/>
<id>urn:sha1:5803c59192b7fb05840d0b452eb64d9f997f3d8f</id>
<content type='text'>
Node.drafts_and_autosaves and Node.recently_changed replace inline
query logic in NodesController#drafts/#recent -- pure refactor, no
behavior change for either action. The real reason: the dashboard's
upcoming abridged widgets need the exact same queries the full pages
already use, just limited and (for drafts) sorted with the current
user's own locked nodes first. Better to share one method than let a
widget and a full page quietly drift onto two versions of "what counts
as a current draft."

current_user_id: is an explicit, optional argument rather than a
separate method -- ordering by lock ownership only applies when a user
is given; omitted entirely, it's pure recency, exactly today's
behavior. Needed Arel.sql wrapping a sanitized CASE expression before
.order would accept it -- sanitize_sql_array only vouches for the
values inside the string, a separate Rails safety check still refuses
any string shaped like more than a plain column reference unless it's
explicitly marked as already-vetted.
</content>
</entry>
</feed>
