summaryrefslogtreecommitdiff
path: root/app/controllers
AgeCommit message (Collapse)Author
2026-07-24Give all sessions a uniform absolute lifetime of one weekerdgeist
Enforced at restore via a login-time stamp, written only at genuine logins so the limit stays absolute rather than sliding. The cookie name rotation logs everyone out once at deploy. Second-factor users are deliberately not treated worse than password-only ones.
2026-07-24Complete the login only after the second factorerdgeist
Enrolled users get a pending marker instead of a session after the password step; a valid code through the challenge writes the real session via reset_session. otp_required without enrollment funnels into setup everywhere except the enrollment, user, and login machinery.
2026-07-24Add self-service TOTP enrollment UI and witnessed admin reseterdgeist
2026-07-23Zoom the action log on assetserdgeist
Asset names in summaries and publish deltas link to assets#show with an inline Chronik beside each, suppressed inside that asset's own zoom, per the node convention. assets#show gains a history button. Also renames the details summary (no longer only translations) and moves View Diff onto its own line.
2026-07-23Keep in-editor asset curation off the head, layering it like every editerdgeist
ensure_autosave! gives body keystrokes and asset curation one shared layer, so head is never mutated in place and every curation change surfaces in the publish delta. Stale rendered join ids are mapped across the clone via asset_id. Curation now requires holding the lock; a missing lock answers 423, matching the autosave endpoint.
2026-07-23Witness asset uploads and out-of-band attaches in the action logerdgeist
2026-07-23Witness asset destruction, naming every node it stripserdgeist
2026-07-23Convert the log's node zoom to participants, completing subtree historieserdgeist
2026-07-22Add attached nodes subsection in assets#show and thumbnail in nodes#createerdgeist
2026-07-22Make important flash notices survive the boilerplate Node-locked info in nodes controllererdgeist
2026-07-22Implement controller and view side of the attach logicerdgeist
2026-07-22Retire nodes#recent, superseded by the action logerdgeist
2026-07-21PDFs currently should have no credits attached to them in displayerdgeist
2026-07-21More PDF headline eligibilityerdgeist
2026-07-21Resolve preview's page-to-node lookup for every layererdgeist
pages_controller#preview loaded a page by id and rendered it directly, assuming @page.node was always present. True for head and draft, since both carry a real node_id: false for autosave, which deliberately has node_id: nil (the mechanism that excludes it from Node#pages and the revision count). A node with an autosave and no draft underneath it: lock, start editing, never explicitly save a draft: loads that autosave directly with no node at all, and any view code assuming @page.node is real (headline image, credits, gallery links) raises. When @page has no node, resolve it by checking autosave_id, then draft_id, then head_id on Node, and set @page.node in memory only: the autosave's node_id: nil is never persisted differently. The existing swap from draft to a fresher autosave, when both exist, is unchanged. Three tests: draft with a fresher autosave on top, autosave with no draft underneath, and the ordinary head-only case with neither.
2026-07-21Remove unreachable image_box drag-and-drop UIerdgeist
image_interface and meta_data (admin_interface.js), the sort_images action and route, and their admin.css rules -- the old vertical image_browser sidebar this all depended on isn't rendered by any current view. related_assets.js and the star-toggle sidebar fully supersede what this did. Confirmed unreachable via full grep across app/, config/routes.rb, and admin_bundle.js before removal -- route, controller action, and JS handler were all individually real and functional, just with nothing left anywhere that could ever trigger them.
2026-07-21Render asset credit inside the gallery lightbox correctlyerdgeist
GLightbox's own selector-based description feature never actually resolved a selector in three different attempts -- literal text, then a resolved-but-empty div, then apparently not firing at all against the installed (unversioned) build. Replaced with a small afterSlideLoad handler that reads a plain data-credit-selector attribute and copies the real element's HTML into the slide's description area directly, depending only on GLightbox handing back a real DOM node per slide -- a much smaller contract than trusting its own caption-resolution feature. Renamed the credit div's class from glightbox-desc, since that name is reserved by the library's own bundled CSS (display: none !important) and can't be reliably overridden from here. Also: distinguishes the credit line and the "N Bilder" caption from ordinary body text (italic, muted, matching .author_and_date's existing convention), and makes the lightbox's caption card follow the site's light/dark toggle via Canvas/CanvasText, since the library's bundled theme never adapts to it on its own.
2026-07-21Whitespace fixerdgeist
2026-07-20Make headline images explicit, add asset creditserdgeist
- related_assets gains a `headline` boolean (DB-enforced: at most one per page), replacing "first image by position" as the headline rule. A rake task backfills the current first image on every live head/draft, so nothing changes visually until an editor changes it. - The image picker sidebar gets a star toggle reflecting the flag; the TinyMCE inline-image picker's badge now reads it too, instead of assuming position 0. - No headline chosen (or none attached) now falls back to the gallery-count caption itself becoming the lightbox trigger, instead of the gallery being unreachable. - Assets gain creator, source_url, and license_key (against a new config/asset_licenses.yml dictionary). asset_credit renders a degrading attribution line, reused as a hidden per-image glightbox caption so credit is one click away for every image, not only the headline's always-visible one. - Fixed: asset thumbnails rendered unconditionally regardless of whether a real variant exists on disk. Asset#has_variant? checks file existence, not content type -- some legacy PDFs have real pre-rewrite thumbnails a content-type check would have hidden. - assets#new/edit rebuilt onto the same node_description/node_content layout as assets#show, picking up the three new fields in the process.
2026-07-19Log CSP violations to their own file instead of relying on Rails.loggererdgeist
2026-07-19Add a 500er logger and a trip wire in admin/boom to test iterdgeist
2026-07-19Fix render gallery rename issue. We saw 500serdgeist
2026-07-18Emit a report-only Content-Security-Policy with nonced inline scriptserdgeist
- dark-mode restore now travels nonced, the admin constants likewise - AUTH_TOKEN deleted in favour of the csrf meta tag - new report collector at /csp_reports
2026-07-18Annotate the History list with each revision's lifecycleerdgeist
Each row in nodes#show's History section now carries terse badges from the action log: created, published, and restored (rollback re-promotion), with date and actor, rendered from entry metadata alone. Backfilled entries wear the inferred marker, so reconstructed provenance stays distinguishable from witnessed history. A revision that was published and later restored shows both badges chronologically -- its true biography. Only create and publish entries carry page_id; trash, restore, and destroy annotate the node's own log zoom instead of any single revision, by design.
2026-07-18Add Trash affordances: cockpit, listing, dashboard entryerdgeist
nodes#show gains a Trash section on trashed nodes: provenance from the trash entry, a restore form whose parent picker pre-fills the old parent while it still lives, and permanent deletion. A Move-to-Trash button joins the status actions on living nodes. nodes#trashed lists trashed subtree roots with weight, provenance, and deletion; the dashboard housekeeping row links to it, and trash/destroy redirect there. Deletion from Trash now removes the whole subtree, deepest first, each node through a real destroy! so every per-node cascade runs -- amending the never-recursive rule for this one sanctioned path (both confirms state the count; the root entry carries destroyed_descendants). Bare Node#destroy still refuses children.
2026-07-17Route and control trash, restore, and permanent deletionerdgeist
2026-07-17Retire recent changes widget, replace with action logerdgeist
2026-07-17Cutover to new Recent Changes widgeterdgeist
2026-07-16Split current_unique_name in a setter and an accessorerdgeist
Also clean up some instance variable assignments on the way.
2026-07-16Add a reader for the action log at admin/logerdgeist
NodeActionsController#index lists entries newest-first, filterable by node_id or user_id -- the two zoom shapes the log was designed around. Rendering goes through NodeActionsHelper.action_summary, which builds one sentence per entry from metadata alone, so entries referencing deleted users or nodes render from their snapshots; live associations only upgrade names to links. Unknown verbs degrade to a generic sentence rather than an error, since the log outlives its vocabulary. The helper is the escaping boundary: every metadata value passes through h() before assembly. Actor names link to the log's own user zoom rather than the unused users page -- inspecting a suspicious user's other actions is the intended workflow. Publish entries with a translation_diff expose a collapsed per-locale change table linking out to the revision itself. Sentences live in en.yml/de.yml following the existing widget-string convention. nodes#show links to its node's zoomed log.
2026-07-16Record the full lifecycle contract in NodeAction entrieserdgeist
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.
2026-07-16Provide parent_unique_name in nodes#new if parent is already pre-seterdgeist
2026-07-15Add NodeAction: an append-only log of who did what to a nodeerdgeist
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.
2026-07-14Supply related-asset URLs/name and public inline-image CSSerdgeist
RelatedAssetsController#create's JSON includes large_url, original_url, and name alongside thumb_url/url; the related-asset list items in both editors carry them as data-large-url, data-original-url, and data-name. ccc.css gets the public-facing .inline-image--full/--half/--left/ --right rules -- admin.css and TinyMCE's content_style cover the cockpit and in-editor renderings respectively.
2026-07-14Make revisions locale awareerdgeist
2026-07-14Store return_to before resetting session in loging. Fixes returning to an admin page before logging inerdgeist
2026-07-13Remove redundant default translation from the translations subsectionerdgeist
2026-07-13Make revision diffs locale-aware, defaulting to whichever locale changederdgeist
diff_against compared title/abstract/body under whatever I18n.locale happened to be ambient, with no concept of 'diff this translation specifically' -- so a change confined to one locale was invisible to Diff Head vs. Draft regardless of which locale you were looking at when you clicked it, exactly the 'yields nothing' complaint from earlier this session. Page#diff_against gains a locale: keyword, additive only -- nil preserves the exact original ambient-locale behavior every existing caller and test already depends on; passing a locale switches to reading each side's actual PageTranslation row directly, same fallback-free reasoning as Page#translation_summary. Page#locale_diff_summary reports one entry per locale present on either side, so an added or removed translation counts as a change even where content matches everywhere it exists on both. RevisionsController#diff now resolves a real locale before diffing -- defaulting to whichever locale actually changed, falling back to the default locale only when nothing did -- and the view carries that locale through every existing control (view toggle, layer-pair buttons, the revision-select form) so it and the view/layer-pair axis stay independently selectable rather than resetting each other.
2026-07-13Fix shared preview redirecting to admin for an ordinary new drafterdgeist
superseded/currently_public used published_at presence as a proxy for page-row relevance, but published_at is carried forward onto every new draft descended from previously-published content (Page#clone_attributes_from, Node#save_draft!) -- so it read true for any draft on an already-published node, whether or not that specific draft had ever gone live itself. Compare draft_id/head_id identity directly instead; no timestamp involved. A stale (superseded) link now redirects to the live public page rather than an admin URL an anonymous holder can't reach anyway.
2026-07-13Wire real autosave into translation editingerdgeist
The shared TinyMCE setup initializes cccms.setup_autosave() on any page with a textarea.with_editor, unconditionally starting a 7-second interval that submits to the form's data-autosave-url -- which the translation edit form never set, so the interval PUT the current page URL itself and 404'd on a nonexistent route. Fixed by actually giving it something to talk to, rather than suppressing it: a real autosave endpoint, and update now goes through Node#autosave!/#save_draft! -- the same pipeline the primary editor uses, fixed earlier this session for exactly this kind of cross-locale carryover. This was the autosave-buffer parity already flagged as due after the proof of concept; the shared JS just forced the timing.
2026-07-13Add per-node translation management: list, edit, destroy, compareerdgeist
Replaces the old locale-switch-and-edit-the-same-screen workflow, which conflated presentation locale with content locale and let an editor silently drift into editing the wrong language with no persistent signal that anything had changed. Non-default-locale content now has its own explicit routes and screens, never sharing a route param with the ambient chrome locale. - PageTranslationsController: index/show/edit/update/destroy, scoped to Page.non_default_locales; update handles first-time creation too, so there's no separate new/create step. - Reads go through the actual PageTranslation row (Page#translation_summary), never through the Globalize fallback-bearing accessor -- fallback is correct for public rendering but wrong for editing, where a missing translation needs to look empty, not borrowed from another locale. - Translations ride on the page's own draft/head cycle; no independent publish state. - nodes#show gains a Translations section (per-locale Lock+Edit / Create+Lock, Destroy, a link into the read-only Compare view) and a locale indicator on its own default-locale content; nodes#edit, nodes#update, and nodes#autosave are pinned to the default locale via Globalize.with_locale regardless of the ambient route locale. - nodes#show no longer double-loads the node or calls wipe_draft! on every view (see previous commit for why that's now safe). - .preview_link_row is renamed .aligned_action_row now that it has a second real consumer.
2026-07-13Asset picker: attach/detach/reorder UI, read-only view, autosave fixerdgeist
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 <template> -- 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.
2026-07-13Add loadOnFocus to initSearchPicker; asset search shows recent by defaulterdgeist
initSearchPicker's input handler has always short-circuited on an empty term -- correct for every existing picker, none of which want results appearing before anything's typed. The asset picker does want exactly that (show the last few uploaded images without requiring a search first), so this adds an opt-in loadOnFocus option instead of changing the shared default: it fires once when the input gains focus with no term yet, reusing the same request/render path a real search uses rather than a separate code path. The AJAX call itself was pulled out into a named runSearch function so both triggers could share it without duplicating the success/render logic. RelatedAssetsController#search now treats a blank term as "show the most recently created, unattached images" (limit 5) rather than returning nothing, matching the new client behavior. A real search term still returns up to 10 name-matched results, unchanged.
2026-07-13Add RelatedAssetsController: search, attach, detach, reordererdgeist
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."
2026-07-12Rebuild the admin dashboard: icon nav, search, signposts, widgetserdgeist
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.
2026-07-12Add a grouped tag+node search endpoint for the upcoming dashboarderdgeist
AdminController#dashboard_search returns tags and nodes as separate, labeled groups (via ActsAsTaggableOn::Tag.named_like and Node.editor_search) rather than the flat per-node list every existing picker returns. initSearchPicker gains a renderResults callback option that, when given, replaces the default per-node rendering entirely -- lets the dashboard render its two labeled groups without a second, parallel picker implementation. resultsHeaderHtml (the "Press Enter to see all results" hint) is now threaded through as a third argument to renderResults, so a picker with custom rendering can still show it -- previously only the default per-node branch ever did. Tags link straight to the existing /admin/nodes/tags/:tag view rather than becoming an in-place filter chip.
2026-07-12Extract drafts/recent query logic from controller into Nodeerdgeist
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.
2026-07-12Unify the four field-level pickers, fix search visibilityerdgeist
menu_items/parent_search/move_to_search/event_search each duplicated their own debounce-free AJAX call and result rendering. Replaced with one shared initSearchPicker, taking each picker's distinctive behavior (parent_search's live path preview, event_search's title hint) as a callback rather than a whole reimplementation. Adds a real debounce with an out-of-order-response guard -- none of the four had either before. admin_search (the Alt+F top-bar search) now shares the same function via its own url/isActive/header options, gaining the same guard and fixing an inconsistency of its own (it previously always slid its panel open, even on zero results). Each picker's results container gets its own id instead of sharing was ever supposed to be on screen at once, an assumption with no actual enforcement behind it. Styling moved from an id-pair (#menu_search_results, #search_results) to a shared .search_results class so a future picker never needs this file touched again. menu_search and the admin top-bar search now call Node.editor_search instead of the public, head-only Node.search -- both are admin-only, authenticated views, and had no reason to inherit the public search's "can't find a draft" limitation. The always-ignored :per_page => 1000 on the latter is gone too; Node.search's second argument was never read by the method at all. Also removed a stale #metadata a { text-transform: lowercase } rule, found while verifying the above -- written for the pre-subnav-removal expand-toggle, which no longer exists; it had been silently lowercasing nodes#edit's own, unrelated #metadata div (including move_to_search's results) by id coincidence ever since. #main_navigation and #overview_toggle intentionally left capitalized rather than special-cased -- both belong to the nav bar already slated to shrink to three icons, not worth polishing on the way out.
2026-07-12Give the sitemap its own view, with collapse and descendant countserdgeist
Extracted from admin#index's inline table into NodesController#sitemap. Nested <details>/<summary> per branch, one linear pass over the existing flat [node, level] list (no added queries) -- each node's own descendant count computed the same way, via a small stack rather than re-walking the tree per node. Branches under updates/, club/erfas, club/chaostreffs, and disclosure start collapsed by default (CccConventions::SITEMAP_COLLAPSED_PATHS); any branch currently collapsed, whether by that default or because someone just closed it, is highlighted via a plain :not([open]) selector -- no state tracked outside the DOM itself. Dropped the update?-post exclusion this view used to rely on -- no longer needed now that updates/ collapses instead of being filtered out, so its real children (previously silently absent) now show up correctly. admin#index's own, separate @sitemap query is unchanged; that view has no collapse mechanism to compensate and wasn't part of this.
2026-07-11Add drafts/recent/mine/chapters admin node viewserdgeist
Four NodesController actions -- drafts, recent, mine, chapters -- each building its own base scope, sharing one private method (index_matching) for search narrowing and pagination. Wizard rewrite to link into these instead of rendering its own tables is a separate, later step. Node.editor_search backs the shared "q" narrowing: an ILIKE substring match against title/abstract on whichever of head or draft is present, splitting the term on whitespace and requiring every word to match somewhere independently, not as one phrase, since real words can end up separated by markup in the underlying HTML. Deliberately separate from Node.search, the public content search, which stays tsvector-based and head-only. chapters generalizes into /admin/nodes/tags/:tags for an arbitrary tag list (OR'd, not AND'd), sharing the controller action but rendering its own template rather than branching inside one view.