The SecurityHeadersMiddleware applied a strict style-src policy to all
responses, blocking inline styles that Wagtail admin relies on for
layout. Skip the custom CSP for /cms/ and /django-admin/ paths.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Makefile used bare 'docker compose' which picks up the dev
docker-compose.yml when run from the app directory on prod. Point
it at the absolute path to docker-compose.prod.yml instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace nav inline newsletter form with Subscribe CTA link per wireframe
- Remove newsletter form from footer; add Connect section with social/RSS links
- Fix honeypot inputs using hidden attribute (inline style blocked by CSP)
- Add available_tags to HomePage.get_context for Explore Topics section
- Add data-comment-form attribute to main comment form for reliable locating
- Seed approved comment in E2E content for reply flow testing
- Expand test_comments.py: moderation message, not-immediately-visible,
missing fields, reply form visible, reply submission
- Make COMMENT_RATE_LIMIT_PER_MINUTE configurable; set 100 in dev to prevent
E2E test exhaustion; update rate limit unit test with override_settings
- Update newsletter/home E2E tests to reflect nav form removal
- Update unit test to assert no nav/footer newsletter forms
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Django 5.1+ removes STATICFILES_STORAGE in favour of the STORAGES dict.
The old setting was silently ignored on Django 5.2, causing StaticFilesStorage
(the default) to be used instead of CompressedManifestStaticFilesStorage.
Result: no content-hashed filenames, no staticfiles.json manifest, and
Cloudflare caching /static/css/styles.css indefinitely with no cache
busting on deploy.
Fix: use STORAGES in base.py (CompressedManifestStaticFilesStorage) and
development.py (plain StaticFilesStorage, whitenoise disabled in dev).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
style-src and font-src were 'self' only, blocking fonts.googleapis.com
stylesheet and fonts.gstatic.com font files. Add both origins so
Space Grotesk, Inter and Fira Code load correctly in production.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Typography: install @tailwindcss/typography and add to plugins so prose
classes render correctly in article/about/legal templates.
2. Callout block: fix icon branches to match CalloutBlock.ICON_CHOICES
(info/warning/trophy/tip). Previous template branched on error/success
which are unreachable; info fell through to else silently.
3. Nav newsletter feedback: remove 'hidden' class from desktop nav
data-newsletter-message element. JS sets textContent only; hidden
class prevented message from ever being visible.
4. Popular Articles sidebar: add numbered Popular Articles widget to home
page sidebar matching wireframe, using latest_articles context with
alternating cyan/pink number accents and read_time_mins.
Rebuild CSS: typography plugin grows output from 24KB to 47KB.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Playwright strict mode requires exactly one match for form[data-newsletter-form]
inside nav. Having desktop + mobile forms both inside <nav> caused a strict
violation. Mobile menu div is now outside the closing </nav> tag; JS toggle
uses getElementById so position doesn't matter.
Rebuild CSS to match template changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- nav: add functional mobile menu panel with JS toggle
- nav: hamburger now shows/hides mobile-menu with aria-expanded state
- about_page: full styled layout (header, prose body, author aside)
- legal_page: full styled layout (header with last-updated, max-w-3xl prose)
- article: fix aside newsletter label to 'Subscribe' for E2E test
- CSS: rebuild after all template changes (4.8KB → 24.3KB)
Committed CSS must match CI build — rebuilt after ALL template edits
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- nav: replace hidden newsletter form + Subscribe link with visible inline form
- nav: fix theme toggle aria-label to 'Toggle theme' (was 'Toggle Dark Mode')
- article_page: wrap share buttons in <section aria-label='Share this article'>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
docker compose stats the cwd when parsing compose files; if cwd is
not accessible to the deploy user the command fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
deploy user has no sudo for systemctl. Instead:
- Use 'docker compose up -d --force-recreate' to recreate the web
container without needing systemctl
- Change Restart=always so systemd re-attaches after the container
is recreated
- Replace 'sudo journalctl' with 'docker compose logs' in error path
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ALLOWED_HOSTS doesn't include localhost, so curl with default Host
header always gets a 400 and the health check fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
YAML folded block scalar (>) was preserving newlines for more-indented
continuation lines, so gunicorn received no arguments and defaulted to
binding on 127.0.0.1:8000. Replace with an explicit entrypoint script
so all args are passed correctly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The compose file lives in /srv/sum/nohype/ while the code is in
/srv/sum/nohype/app/ so the Dockerfile is at app/Dockerfile.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove WhiteNoise from MIDDLEWARE in development: it intercepts /static/
requests and serves from STATIC_ROOT, which is empty without collectstatic.
Django's runserver serves static files natively with DEBUG=True.
- Switch to StaticFilesStorage in dev: no manifest required.
- Add media URL pattern in DEBUG mode: runserver does not serve MEDIA_ROOT
automatically, so uploaded images were 404ing in local dev.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CompressedManifestStaticFilesStorage requires collectstatic to generate a
manifest before it can serve anything. Dev containers never run collectstatic
so every static asset 404s. Override to StaticFilesStorage in dev so Django
serves files directly from STATICFILES_DIRS and app static directories.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wagtail's initial migration creates a localhost:80 site. Wagtail matches
incoming requests by hostname before ever checking is_default_site, so
updating only the is_default_site record left localhost:80 still pointing
at the Welcome page. Fix by updating root_page on every site.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- /opt/playwright-tools/browsers only exists on agent-workspace; mounting it
in docker-compose.yml breaks local dev for everyone else
- seed_e2e_content is idempotent so safe to run on every startup; removes the
manual step that nobody knew about
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wagtail initialises the default site with hostname 'localhost'. The previous
get_or_create on '127.0.0.1' left the localhost site intact (still pointing
to the Welcome page), so browsers got the wrong root page.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Mount /opt/playwright-tools/browsers into web container (docker-compose.yml
and CI docker run) — never download browsers, use the ones on this host
- Set PLAYWRIGHT_BROWSERS_PATH in all container envs (compose + CI)
- Drop 'playwright install chromium' steps from pr-e2e and nightly-e2e jobs
- Bump playwright requirement to ~1.57.0 to match the installed browser builds
- Fix seed_e2e_content: de-duplicate default Site entries left by unit test
fixtures so Wagtail always routes to the seeded home page
- Fix test_comments_section_absent_when_disabled: use exact=True on heading
locator to avoid matching 'No Comments Article' h1 as 'Comments' heading
- Fix test_copy_link_button_updates_text: use [data-copy-link] data-attr
locator (stable across text change) and force-override clipboard.writeText
via page.evaluate() rather than relying on init_script polyfill
E2E suite verified locally: 34 passed via docker compose exec
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- test_homepage_title_contains_brand: to_have_title() requires a string or
regex, not a lambda; switch to re.compile('No Hype AI')
- test_granular_preferences_save_dismisses_banner: wrong element clicked to
open <details>; use 'details summary' locator directly
- test_subscribe_invalid_email_shows_error: browser HTML5 email validation
swallows the submit event before the JS handler fires; add 'novalidate' via
evaluate() so the fetch still runs and the server returns 400
- test_copy_link_button_updates_text: clipboard API unavailable in headless
Docker; add polyfill + pre-grant permissions in conftest page fixture so
the JS success path runs and button text becomes 'Copied'
- test_comments_section_absent_when_disabled: guard against Wagtail's
add_child() resetting BooleanField defaults by calling an explicit
.update(comments_enabled=False) + re-setting on the instance before
save_revision().publish(); also tighten test to assert 200 + correct title
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Chains 'migrate --noinput' before 'runserver' in the web
service command so migrations are applied automatically on
'docker compose up'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>