# Maintenance Mode Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a toggle-able maintenance mode that redirects all public pages to a standalone "coming soon" page with waitlist signup.

**Architecture:** New `maintenanceMode` boolean on `SiteSettings` entity, a kernel event subscriber that redirects non-admin requests when enabled, a standalone `/wkrotce` page with branding and waitlist form, and an admin toggle in the existing settings page.

**Tech Stack:** Symfony 8.0, Doctrine ORM, Twig (standalone template), Stimulus (waitlist controller), Cloudflare Turnstile

---

## Chunk 1: Backend

### Task 1: Add maintenanceMode field to SiteSettings entity

**Files:**
- Modify: `src/Entity/SiteSettings.php`

- [ ] **Step 1: Add field + getter + setter**

After the `$enabledLocales` property, add:

```php
#[ORM\Column(type: 'boolean', options: ['default' => true])]
private bool $maintenanceMode = true;

public function isMaintenanceMode(): bool
{
    return $this->maintenanceMode;
}

public function setMaintenanceMode(bool $enabled): self
{
    $this->maintenanceMode = $enabled;

    return $this;
}
```

- [ ] **Step 2: Commit**

```bash
git add src/Entity/SiteSettings.php
git commit -m "Add maintenanceMode field to SiteSettings entity"
```

### Task 2: Generate Doctrine migration

**Files:**
- Create: `migrations/VersionXXXX.php` (auto-generated)

- [ ] **Step 1: Generate migration**

Run: `php bin/console doctrine:migrations:diff`

- [ ] **Step 2: Verify migration SQL**

The migration should contain:
```sql
ALTER TABLE site_settings ADD maintenance_mode BOOLEAN DEFAULT 1 NOT NULL
```

- [ ] **Step 3: Run migration locally**

Run: `php bin/console doctrine:migrations:migrate --no-interaction`

- [ ] **Step 4: Commit**

```bash
git add migrations/
git commit -m "Add migration for maintenance_mode column"
```

### Task 3: Add isMaintenanceMode() to SiteSettingsService

**Files:**
- Modify: `src/Service/SiteSettingsService.php`

- [ ] **Step 1: Add proxy method**

After `isLocaleEnabled()`, add:

```php
public function isMaintenanceMode(): bool
{
    return $this->get()->isMaintenanceMode();
}
```

- [ ] **Step 2: Commit**

```bash
git add src/Service/SiteSettingsService.php
git commit -m "Add isMaintenanceMode() to SiteSettingsService"
```

### Task 4: Create MaintenanceSubscriber

**Files:**
- Create: `src/EventListener/MaintenanceSubscriber.php`

- [ ] **Step 1: Create the subscriber**

Follow the same pattern as `LocaleSubscriber.php`:

```php
<?php

namespace App\EventListener;

use App\Service\SiteSettingsService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class MaintenanceSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private SiteSettingsService $settings,
        private UrlGeneratorInterface $urlGenerator,
        private AuthorizationCheckerInterface $authChecker,
        private string $environment,
    ) {}

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => [['onKernelRequest', 10]],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        if (!$this->settings->isMaintenanceMode()) {
            return;
        }

        $path = $event->getRequest()->getPathInfo();

        if (str_starts_with($path, '/admin')) {
            return;
        }

        if ($path === '/wkrotce' || str_starts_with($path, '/api/waitlist')) {
            return;
        }

        if ($this->environment === 'dev' && (str_starts_with($path, '/_profiler') || str_starts_with($path, '/_wdt'))) {
            return;
        }

        try {
            if ($this->authChecker->isGranted('ROLE_ADMIN')) {
                return;
            }
        } catch (\Throwable) {
            // No firewall context (e.g. asset requests) — proceed with redirect
        }

        $event->setResponse(new RedirectResponse(
            $this->urlGenerator->generate('app_coming_soon'),
            302
        ));
    }
}
```

Note: The `$environment` parameter will be autowired via `#[Autowire('%kernel.environment%')]` — but since Symfony 8 auto-wires scalar params by name from container params, we need the attribute. Update constructor:

```php
use Symfony\Component\DependencyInjection\Attribute\Autowire;

public function __construct(
    private SiteSettingsService $settings,
    private UrlGeneratorInterface $urlGenerator,
    private AuthorizationCheckerInterface $authChecker,
    #[Autowire('%kernel.environment%')] private string $environment,
) {}
```

- [ ] **Step 2: Commit**

```bash
git add src/EventListener/MaintenanceSubscriber.php
git commit -m "Add MaintenanceSubscriber to redirect public pages in maintenance mode"
```

### Task 5: Create ComingSoonController

**Files:**
- Create: `src/Controller/ComingSoonController.php`

- [ ] **Step 1: Create the controller**

```php
<?php

namespace App\Controller;

use App\Entity\Waitlist;
use App\Service\SiteSettingsService;
use App\Service\TurnstileService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ComingSoonController extends AbstractController
{
    #[Route('/wkrotce', name: 'app_coming_soon')]
    public function index(
        SiteSettingsService $settingsService,
        TurnstileService $turnstile,
        EntityManagerInterface $em,
    ): Response {
        if (!$settingsService->isMaintenanceMode()) {
            return $this->redirectToRoute('app_home');
        }

        return $this->render('coming_soon/index.html.twig', [
            'waitlist_count' => $em->getRepository(Waitlist::class)->count([]),
            'turnstile_site_key' => $turnstile->getSiteKey(),
        ]);
    }
}
```

- [ ] **Step 2: Commit**

```bash
git add src/Controller/ComingSoonController.php
git commit -m "Add ComingSoonController for /wkrotce route"
```

### Task 6: Add maintenance toggle to admin settings

**Files:**
- Modify: `src/Controller/Admin/SettingsController.php`
- Modify: `templates/admin/settings/index.html.twig`

- [ ] **Step 1: Update controller POST handling**

In `SettingsController::index()`, after the `setMonetizationEnabled` line, add:

```php
$settings->setMaintenanceMode($request->request->getBoolean('maintenance_mode'));
```

- [ ] **Step 2: Add toggle to template**

In `templates/admin/settings/index.html.twig`, add a new section **before** the "Widoczność sekcji" block (before line 17). Insert after the `<input type="hidden" name="_token">` line:

```twig
{# ── Tryb konserwacji ── #}
<div class="bg-white rounded-2xl p-8 border border-gray-200 mb-6">
    <h2 class="text-xl font-bold text-dark mb-6">Tryb konserwacji</h2>

    <label class="flex items-center justify-between py-4">
        <div>
            <p class="font-semibold text-dark">Strona „Wkrótce startujemy"</p>
            <p class="text-sm text-gray-500 mt-1">Gdy włączony, wszystkie publiczne strony przekierowują na stronę startową z formularzem zapisu</p>
        </div>
        <div class="relative">
            <input type="hidden" name="maintenance_mode" value="0">
            <input type="checkbox" name="maintenance_mode" value="1" {{ settings.maintenanceMode ? 'checked' : '' }}
                class="sr-only peer">
            <div class="w-11 h-6 bg-gray-200 peer-focus:ring-4 peer-focus:ring-scout/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-scout cursor-pointer"></div>
        </div>
    </label>
</div>
```

- [ ] **Step 3: Commit**

```bash
git add src/Controller/Admin/SettingsController.php templates/admin/settings/index.html.twig
git commit -m "Add maintenance mode toggle to admin settings page"
```

## Chunk 2: Frontend — Coming Soon Page

### Task 7: Create standalone coming_soon template

**Files:**
- Create: `templates/coming_soon/index.html.twig`

- [ ] **Step 1: Create the template**

This is a standalone page (does NOT extend base.html.twig). It needs its own `<html>` document with:
- Tailwind CSS (from `var/tailwind/app.built.css`)
- Inter font (self-hosted `/fonts/inter-var.woff2`)
- `{{ importmap('app') }}` for Stimulus controllers (waitlist_controller)
- Turnstile JS

Design:
- Background: cream `#FFFDF7`, full viewport height
- Logo TROPO centered top (use `<img>` directly, not LiipImagine macro since no base template)
- Large bold heading "Startujemy niedługo"
- Subtitle text
- Waitlist form (same Stimulus `data-controller="waitlist"` pattern as download page)
- Character icons (Scout, Blitz, Nova, Echo) as small decorative elements near the bottom
- Waitlist count if > 0
- Turnstile widget with `data-theme="light"`
- CSRF token via `{{ csrf_token('public_form') }}`

```twig
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TROPO — Startujemy niedługo</title>
    <meta name="description" content="TROPO — gra przygodowa dla dzieci. Startujemy niedługo! Zapisz się na listę oczekujących.">
    <link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="icon" href="/favicon.ico" sizes="any">
    <link rel="stylesheet" href="/tailwind/app.built.css">
    {{ importmap('app') }}
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body style="background-color: #FFFDF7; font-family: 'Inter', sans-serif;" class="min-h-screen flex flex-col items-center justify-center px-4 py-12">

    {# Logo #}
    <img src="/images/logo.png" alt="TROPO" class="h-16 mb-8">

    {# Main content #}
    <div class="text-center max-w-lg">
        <h1 class="text-4xl sm:text-5xl font-extrabold text-dark leading-tight">
            Startujemy<br>niedługo
        </h1>
        <p class="mt-4 text-lg text-gray-500">
            Przygoda jest bliżej niż myślisz. Zapisz się, a powiadomimy Cię o starcie.
        </p>

        {% if waitlist_count > 0 %}
        <p class="mt-3 text-sm text-gray-400">
            Już <strong class="text-dark">{{ waitlist_count }}</strong> {{ waitlist_count == 1 ? 'osoba czeka' : 'osób czeka' }} na start
        </p>
        {% endif %}
    </div>

    {# Waitlist form #}
    <div class="mt-8 w-full max-w-md"
         data-controller="waitlist"
         data-waitlist-sending-label-value="Wysyłanie..."
         data-waitlist-submit-label-value="Powiadom mnie"
         data-waitlist-auto-name-value="true">
        <form data-waitlist-target="form" data-action="submit->waitlist#submit" class="space-y-3">
            <input type="hidden" name="name" value="">
            <input type="email" name="email" placeholder="Twój adres e-mail" required
                class="w-full px-5 py-3.5 rounded-xl border border-gray-300 focus:border-scout focus:ring-2 focus:ring-scout/20 outline-none text-sm bg-white">
            <input type="hidden" name="_token" value="{{ csrf_token('public_form') }}">
            <div class="cf-turnstile" data-sitekey="{{ turnstile_site_key }}" data-theme="light"></div>
            <button type="submit" data-waitlist-target="submit"
                class="w-full px-6 py-3.5 bg-scout text-white font-bold rounded-xl hover:bg-scout/90 transition text-lg">
                Powiadom mnie
            </button>
        </form>
        <div data-waitlist-target="success" class="hidden text-center mt-4">
            <div class="text-4xl mb-2">🎉</div>
            <p class="text-lg font-bold text-dark">Dziękujemy! Damy Ci znać.</p>
        </div>
        <p data-waitlist-target="message" class="hidden mt-3 font-semibold text-center"></p>
    </div>

    {# Character icons #}
    <div class="mt-12 flex items-center gap-6 opacity-60">
        <img src="/images/characters/scout_ikona.png" alt="Scout" class="w-12 h-12 rounded-full">
        <img src="/images/characters/blitz_ikona.png" alt="Blitz" class="w-12 h-12 rounded-full">
        <img src="/images/characters/nova_ikona.png" alt="Nova" class="w-12 h-12 rounded-full">
        <img src="/images/characters/echo_ikona.png" alt="Echo" class="w-12 h-12 rounded-full">
    </div>

    <p class="mt-8 text-xs text-gray-400">&copy; 2026 TROPO</p>
</body>
</html>
```

Note: The Tailwind CSS file path should use `{{ asset('tailwind/app.built.css') }}` — but since this is standalone without the asset mapper providing the function fully, use the direct path. Verify the correct public path for the built CSS by checking what `base.html.twig` uses via `{% block stylesheets %}`. Actually, since `{{ importmap('app') }}` is available (it's a Twig function from Symfony), and `app.css` imports Tailwind, the CSS should be loaded via the importmap. Check if a separate `<link>` is needed or if Tailwind CSS is already loaded through the asset pipeline. Adjust accordingly.

- [ ] **Step 2: Verify locally**

Run: `symfony serve` then visit `http://localhost:8000/wkrotce`

Check: page renders with logo, heading, waitlist form, character icons. Form submits successfully via AJAX.

- [ ] **Step 3: Commit**

```bash
git add templates/coming_soon/index.html.twig
git commit -m "Add standalone coming soon page template with waitlist form"
```

### Task 8: Rebuild Tailwind CSS

- [ ] **Step 1: Rebuild**

Run: `php bin/console tailwind:build`

Needed because the new template uses Tailwind classes that may not be in the existing build.

- [ ] **Step 2: Commit if CSS changed**

```bash
git add var/tailwind/app.built.css
git commit -m "Rebuild Tailwind CSS for coming soon page"
```

### Task 9: End-to-end verification

- [ ] **Step 1: Test maintenance ON**

1. Go to `/admin/ustawienia`, enable maintenance toggle, save
2. Open incognito browser, visit `/` — should redirect to `/wkrotce`
3. Visit `/blog` — should redirect to `/wkrotce`
4. Visit `/wkrotce` — should show coming soon page
5. Submit waitlist form — should work (AJAX to `/api/waitlist`)

- [ ] **Step 2: Test maintenance OFF**

1. Go to `/admin/ustawienia`, disable maintenance toggle, save
2. Open incognito, visit `/` — should show homepage normally
3. Visit `/wkrotce` — should redirect to `/`

- [ ] **Step 3: Test admin access during maintenance**

1. Enable maintenance mode
2. As logged-in admin, visit `/` — should show homepage (not redirected)
3. Visit `/admin` — should work normally

- [ ] **Step 4: Final commit with all changes**

```bash
git add -A
git commit -m "Add maintenance mode with coming soon page and admin toggle"
```
