Skip to content

Add dynamic info page editing via Firestore admin panel#2

Closed
AgentKush wants to merge 33 commits into
mainfrom
feature/dynamic-info-page
Closed

Add dynamic info page editing via Firestore admin panel#2
AgentKush wants to merge 33 commits into
mainfrom
feature/dynamic-info-page

Conversation

@AgentKush
Copy link
Copy Markdown
Owner

Closes DonovanMods#25

Summary

  • Info page content is now stored in Firestore (site_content/info_page) and rendered dynamically
  • New admin panel at /admin/info for editing page sections without redeploying
  • Protected by HTTP Basic Auth using the ADMIN_PASSWORD environment variable (no user login system needed)
  • Each section has a title, description (markdown supported), link text, and link URL
  • Sections can be added, removed, and reordered through the admin interface
  • Falls back to the current hardcoded Discord/Upvote content if no Firestore document exists
  • Includes model spec for SiteContent and request spec for admin auth

Setup

Set the ADMIN_PASSWORD environment variable to enable admin access:

ADMIN_PASSWORD=your-secret-password

Then visit /admin/info and enter any username with that password.

Test plan

  • Verify info page loads with default content when no Firestore document exists
  • Set ADMIN_PASSWORD env var and verify /admin/info prompts for Basic Auth
  • Edit sections, save, and verify changes appear on the public info page
  • Add and remove sections via admin panel
  • Verify markdown rendering in section descriptions
  • Confirm 401 when accessing /admin/info without credentials
  • Confirm 503 when ADMIN_PASSWORD is not set
  • Run specs: bundle exec rspec spec/models/site_content_spec.rb spec/requests/admin/info_spec.rb

Generated with Claude Code

@AgentKush AgentKush self-assigned this Apr 4, 2026
@AgentKush AgentKush force-pushed the feature/dynamic-info-page branch from b0024a8 to dc41252 Compare April 4, 2026 16:41
@AgentKush AgentKush force-pushed the feature/dynamic-info-page branch from dc41252 to dc71366 Compare April 4, 2026 16:47
AgentKush and others added 16 commits April 4, 2026 18:10
The Gemfile specifies ruby "3.4.9" but .ruby-version was still
3.4.8, causing CI to install the wrong Ruby version and fail with
"Your Ruby version is 3.4.8, but your Gemfile specified 3.4.9".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CSP initializer was entirely commented out, leaving the application
with no Content-Security-Policy headers. This is a security risk as it
allows unrestricted loading of scripts, styles, and other resources.

Configure CSP with sensible defaults for this app's needs:
- default_src/script_src/connect_src: self + https
- img_src/font_src: self + https + data URIs
- object_src: none (blocks Flash/Java embeds)
- style_src includes unsafe_inline for Tailwind compatibility
- Nonce-based script protection via importmap

Starts in report-only mode so violations are logged without breaking
the site. Once verified in production, report_only can be removed.

Fixes the medium-severity CSP bug reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tool#filename calls url.split("/") without checking if url is nil.
When a Firestore document lacks a fileURL field, url is nil and
calling .split on it raises NoMethodError, crashing the tools
index page.

Add a nil guard that returns nil early when url is absent, and
use URI parsing for consistency with how Mod#filename handles URLs.

Fixes the medium-severity Tool#filename crash reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The author_slug method in the Displayable concern calls
author.parameterize without checking if author is nil. If a single
Firestore document lacks an author field, it raises NoMethodError
and can crash the entire mods or tools index page since author_slug
is called during rendering of every record.

Return "unknown" as a safe fallback slug when author is nil, keeping
the page rendering and producing a valid URL segment.

Fixes the medium-severity author_slug crash reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use string splitting instead of URI() to avoid URI::InvalidURIError
on malformed Firestore URLs, matching the safer approach already used
in Mod#filename (commit 2b66be7).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When switching Tailwind from darkMode: "media" to "class", the OS
preference is no longer automatically respected. This adds a
matchMedia check as fallback when no localStorage preference is
saved, preserving the original behavior for first-time visitors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements array-based pagination for the mods index when displaying
all mods. Search results and author-filtered views show all matches
without pagination, as requested in issue DonovanMods#55.

- Add PaginationHelper with page windowing and ellipsis support
- Add _pagination.html.erb partial with Prev/Next and page numbers
- Paginate at 20 mods per page (configurable via DEFAULT_PER_PAGE)
- Show "page X of Y" counter when paginated
- Styled with existing Tailwind classes for light/dark mode
Covers paginate_array edge cases (empty collection, page clamping,
custom per_page) and PaginationResult methods (first/last page,
navigation, page_range with ellipsis).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .gitignore only had .env.* (matching .env.local, .env.production,
etc.) but not .env itself, so the base .env file was committed and
tracked. While it currently contains non-secret config (project IDs,
bucket names), tracking .env files is a security risk as developers
may add secrets to it later.

Changes:
- Add .env to .gitignore so it is no longer tracked
- Remove .env from git index (file stays on disk for existing devs)
- Add .env.example template so new developers know which vars to set
- Whitelist .env.example in .gitignore so the template is tracked

Fixes the low-severity .env tracking bug reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _mod.html.erb partial passes mod.author (which contains spaces
and mixed case) to mod_detail_path, producing ugly URLs with encoded
spaces like /mods/Donovan%20Young/some-mod.

Change to mod.author_slug which produces clean, parameterized URLs
like /mods/donovan-young/some-mod, consistent with how the show
action and author filtering already work.

Fixes the low-severity ugly URL bug reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The search input debounce was set to 200ms, firing a Turbo Frame
request on nearly every keystroke. Increase to 400ms which still
feels responsive but significantly reduces unnecessary requests
and Firestore reads during active typing.

Fixes the low-severity search debounce bug reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The markdown helper created new CodeRayify and Redcarpet::Markdown
instances on every call. On the mods index this means allocating
these objects once per mod with a README.

Extract into a memoized private method so objects are created once
per request and reused for all subsequent renders.

Fixes the low-severity markdown renderer performance bug in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The require_master_key setting was commented out, meaning the app
can boot in production without a master key. Without it, Rails
credentials (including Firebase keyfile) can't be decrypted, causing
confusing Firestore errors instead of a clear startup failure.

Uncomment so production deploys fail fast if RAILS_MASTER_KEY is
missing. The Kamal deploy config already provides this as a secret.

Fixes the low-severity require_master_key bug reported in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The firestore class method creates a new Google::Cloud::Firestore
client on every call. Memoize with @FireStore ||= so the client is
created once per class and reused. The Google Cloud Firestore client
is designed to be long-lived and thread-safe.

Fixes the low-severity Firestore client performance bug in DonovanMods#76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reset class-level @FireStore instance variable after each test
to prevent RSpec doubles from persisting via Firestorable's
@FireStore ||= memoization pattern. This fixes 8 test failures
where leaked doubles caused errors in subsequent examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@AgentKush AgentKush force-pushed the feature/dynamic-info-page branch from afb555b to b0e5f15 Compare April 4, 2026 17:27
AgentKush and others added 4 commits April 4, 2026 18:33
- Use .positive? instead of > 0
- Break long line in PaginationResult.new
- Use described_class instead of explicit class names in specs
- Fix context descriptions to start with when/with/without

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The template now uses mod.author_slug instead of mod.author for
cleaner URLs, so the test expectation needs to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These are local MCP plugin directories that shouldn't be tracked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces inline toggleTheme() and DOMContentLoaded listener with a
Stimulus theme_controller.js. Uses data-action and data-target
attributes instead of global functions and getElementById, preventing
potential conflicts with other scripts.

The early-load FOUC prevention script in <head> is kept since Stimulus
connects after DOMContentLoaded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge nested conditional into outer if to satisfy RuboCop
Style/SoleNestedConditional rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract filter_by_author, filter_by_query, paginate_mods, and
render_index private methods to bring ABC size and method length
below RuboCop thresholds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AgentKush and others added 11 commits April 4, 2026 18:53
Replace `return unless params[:x].present?` with `return if
params[:x].blank?` per Rails/Blank cop preference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…smatch

Fix .ruby-version to match Gemfile ruby requirement (3.4.9)
…nil-guards-and-csp

Fix/medium severity nil guards and csp
…anup-and-performance

Fix: Code clean-up, security hardening, and performance improvements
Add dark/light theme toggle to header nav
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses issue DonovanMods#25 — the info page content is now stored in Firestore
and editable through an admin interface at /admin/info, protected by
HTTP Basic Auth (ADMIN_PASSWORD env var). No user login system needed.

- SiteContent model reads/writes page sections to Firestore
- Admin::BaseController provides HTTP Basic Auth for admin routes
- Admin::InfoController allows adding, editing, and removing sections
- Info view renders dynamic content with markdown support
- Falls back to default content if no Firestore document exists
- Includes model and request specs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add login form at /admin/login with gold theme styling
- Session expires after 30 minutes of inactivity
- Audit logging for login/logout/failed attempts with IP
- Proper logout button in admin panel
- Updated specs for session-based auth flow

Addresses review feedback about HTTP Basic Auth limitations
(no session management, no audit trail, credentials in every request).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents info page specs from hitting Firestore. Returns nil so
the controller falls back to default_info_sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Override global SiteContent.find stub with and_call_original in spec
  so tests exercise real implementation through mocked Firestore
- Use described_class instead of explicit SiteContent in specs
- Fix array indentation in spec
- Use modifier if for SiteContent stub in firestore support
- Break long lines in base_controller and site_content
- Avoid multi-line block chain in info_controller

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@AgentKush AgentKush force-pushed the feature/dynamic-info-page branch from f63b457 to f3ca7f9 Compare April 5, 2026 00:05
@AgentKush AgentKush closed this Apr 5, 2026
@AgentKush AgentKush deleted the feature/dynamic-info-page branch April 5, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update INFO page dynamically

2 participants