diff --git a/Dockerfile b/Dockerfile index 95f39ce..8763a21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,21 @@ -FROM php:8.4-frankenphp +FROM dunglas/frankenphp:1.12.7-php8.4 ENV APP_ENV=prod \ - COMPOSER_ALLOW_SUPERUSER=1 + COMPOSER_ALLOW_SUPERUSER=1 \ + SERVER_NAME=:8787 RUN apt-get update \ - && apt-get install -y --no-install-recommends git unzip sqlite3 \ + && apt-get install -y --no-install-recommends git unzip sqlite3 libicu-dev \ && rm -rf /var/lib/apt/lists/* -RUN docker-php-ext-install pdo pdo_sqlite opcache bcmath intl +RUN docker-php-ext-install bcmath intl COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /app # composer.json zuerst → Layer-Cache COPY composer.json composer.lock ./ -RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist +RUN composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist --no-scripts COPY . . diff --git a/Doku/plan.md b/Doku/plan.md index faea34a..0345f31 100644 Binary files a/Doku/plan.md and b/Doku/plan.md differ diff --git a/compose.yaml b/compose.yaml index 5515c62..4d94ad5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,16 +4,20 @@ services: container_name: svc-essensplanung restart: unless-stopped env_file: - - .env + # Secrets + Prod-Redirect-URI: APP_SECRET, API_TOKEN, KEYCLOAK_REDIRECT_URI + - .env.local environment: APP_ENV: prod APP_DEBUG: '0' DATABASE_URL: sqlite:////data/essens.db + DEFAULT_URI: https://essen.ljh31.de + KEYCLOAK_ISSUER: https://iam.ljh31.de/realms/family + KEYCLOAK_CLIENT_ID: essen volumes: - essens_data:/data - essens_var:/app/var healthcheck: - test: ['CMD', 'php', '-r', '$c=@fsockopen("127.0.0.1",8787); exit($c?0:1);'] + test: ['CMD', 'php', '-r', '$$c=@fsockopen("127.0.0.1",8787); exit($$c?0:1);'] interval: 30s timeout: 5s retries: 3 diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..3c017f3 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..1220f78 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..760ec76 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000..8966487 Binary files /dev/null and b/public/icons/maskable-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..6a7dd08 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Essensplanung", + "short_name": "Essen", + "description": "Familien-Essensplanung: Planungsliste, Tagesansicht, Inspiration.", + "lang": "de", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#f4eee1", + "theme_color": "#3f6d4e", + "icons": [ + { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..fde559b --- /dev/null +++ b/public/sw.js @@ -0,0 +1,79 @@ +/* Service Worker: App-Shell cachen (Offline + schneller Start). + Version bei jeder Template-/Asset-Änderung bumpen (SHELL_VERSION), + damit alte Caches beim Activate bereinigt werden. */ +const SHELL_VERSION = 'v1'; +const SHELL_CACHE = 'essen-shell-' + SHELL_VERSION; +const SHELL = [ + '/', + '/manifest.webmanifest', + '/js/htmx.min.js', + '/js/sortable.min.js', + '/icons/icon-192.png', + '/icons/icon-512.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(SHELL_CACHE) + .then((cache) => cache.addAll(SHELL)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key.startsWith('essen-shell-') && key !== SHELL_CACHE) + .map((key) => caches.delete(key)) + ) + ) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + if (event.request.method !== 'GET' || url.origin !== self.location.origin) { + return; + } + + // Navigations: erst Netzwerk (frische Deploys!), dann Cache (Offline). + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .then((response) => { + const kopie = response.clone(); + caches.open(SHELL_CACHE).then((cache) => cache.put(event.request, kopie)); + return response; + }) + .catch(() => + caches.match(event.request).then((treffer) => treffer || caches.match('/')) + ) + ); + return; + } + + // Statische Assets: Cache-first (unveränderlich pro Version). + if ( + url.pathname.startsWith('/js/') || + url.pathname.startsWith('/icons/') || + url.pathname === '/manifest.webmanifest' + ) { + event.respondWith( + caches.match(event.request).then( + (treffer) => + treffer || + fetch(event.request).then((response) => { + const kopie = response.clone(); + caches.open(SHELL_CACHE).then((cache) => cache.put(event.request, kopie)); + return response; + }) + ) + ); + } +}); diff --git a/src/UI/Http/Auth/OidcAuthController.php b/src/UI/Http/Auth/OidcAuthController.php index 2ac7fd3..6c413c6 100644 --- a/src/UI/Http/Auth/OidcAuthController.php +++ b/src/UI/Http/Auth/OidcAuthController.php @@ -65,7 +65,8 @@ final class OidcAuthController try { $tokens = $this->oidc->exchangeCode($code); $claims = $this->oidc->verifyIdToken($tokens['id_token']); - } catch (\Throwable) { + } catch (\Throwable $e) { + error_log('OIDC-Callback fehlgeschlagen: ' . $e->getMessage()); return new RedirectResponse($this->router->generate('login')); } diff --git a/src/UI/Http/Auth/OidcClient.php b/src/UI/Http/Auth/OidcClient.php index 7f125c7..0a6c87f 100644 --- a/src/UI/Http/Auth/OidcClient.php +++ b/src/UI/Http/Auth/OidcClient.php @@ -41,7 +41,6 @@ final class OidcClient 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, 'scope' => implode(' ', $this->scopes), - 'prompt' => 'login', ]; if ($state !== null && $state !== '') { $q['state'] = $state; @@ -71,7 +70,8 @@ final class OidcClient } $resp = $this->httpClient->request('POST', $this->issuer . '/protocol/openid-connect/token', [ - 'json' => $body, + 'body' => http_build_query($body), + 'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'], 'timeout' => 10, ]); $sc = $resp->getStatusCode(); @@ -198,10 +198,9 @@ final class OidcClient $mod = $this->derInteger($n); $exp = $this->derInteger($e); - $rsakey = - "\x30" . $this->derLen(strlen($mod) + strlen($exp)) - . "\x02" . $this->derLen(strlen($mod)) . $mod - . "\x02" . $this->derLen(strlen($exp)) . $exp; + $modTLV = "\x02" . $this->derLen(strlen($mod)) . $mod; + $expTLV = "\x02" . $this->derLen(strlen($exp)) . $exp; + $rsakey = "\x30" . $this->derLen(strlen($modTLV) + strlen($expTLV)) . $modTLV . $expTLV; // rsaEncryption OID 1.2.840.113549.1.1.1 → DER: 2A 86 48 86 F7 0D 01 01 01 $algId = "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01"; diff --git a/src/UI/Http/Essen/MahlzeitController.php b/src/UI/Http/Essen/MahlzeitController.php index 864e49e..dbee106 100644 --- a/src/UI/Http/Essen/MahlzeitController.php +++ b/src/UI/Http/Essen/MahlzeitController.php @@ -78,9 +78,18 @@ final class MahlzeitController extends AbstractController return $this->render('mahlzeiten/tage.html.twig', [ 'tage' => $this->gruppierenNachTag($mahlzeiten), 'total' => \count($mahlzeiten), + 'heute' => $this->heutigerTag(), ]); } + /** Aktueller Wochentag (Europe/Berlin) als `Wochentag`-Name (z. B. "Di"). */ + private function heutigerTag(): string + { + $iso = (int) (new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')))->format('N'); + + return Wochentag::cases()[$iso - 1]->name; + } + /** * POST /mahlzeiten/reorder — neue Reihenfolge der Planungsliste (Drag & Drop) * persistieren. Erwartet JSON `{"ids": [3, 1, 2]}` oder das Formularfeld diff --git a/src/UI/Http/ExceptionSubscriber.php b/src/UI/Http/ExceptionSubscriber.php index d4adef1..3aa1b27 100644 --- a/src/UI/Http/ExceptionSubscriber.php +++ b/src/UI/Http/ExceptionSubscriber.php @@ -47,7 +47,7 @@ final class ExceptionSubscriber implements EventSubscriberInterface return; } - if ($e instanceof HttpExceptionInterface && $api) { + if ($e instanceof HttpExceptionInterface) { $event->setResponse( $this->render($request, $e->getStatusCode(), $this->shortName(get_class($e)), $e->getMessage(), $wantsJson) ); diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig index f88958b..ef792c1 100644 --- a/templates/home/index.html.twig +++ b/templates/home/index.html.twig @@ -3,11 +3,15 @@ {% block title %}Start · svc-essensplanung{% endblock %} {% block body %} -

{{ name }}

+

Willkommen, {{ name }}!

+

Deine Woche auf einen Blick – was steht an und was schmeckt mal wieder?

+
-

Willkommen zur Essensplanung! (M0-Skelett)

-

E-Mail: {{ email }}

-

Redirect-Pfad: {{ redirect }}

-

M1: CRUD-Slice (Dish-Create/Update/Delete/List) folgt.

+
+ Zur Planungsliste + Inspiration + Archiv +
+

Angemeldet als {{ email }}

{% endblock %} diff --git a/templates/layout/base.html.twig b/templates/layout/base.html.twig index 51c60f6..53423a1 100644 --- a/templates/layout/base.html.twig +++ b/templates/layout/base.html.twig @@ -2,102 +2,423 @@ - + {% block title %}svc-essensplanung{% endblock %} + + + + + + + {% block stylesheets %}{% endblock %}
- essen.ljh31.de + + + Essensplanung +
+ {% block fab %} + + + + {% endblock %}
{% for label, messages in app.flashes %} {% for message in messages %} @@ -108,5 +429,12 @@
{% block javascripts %}{% endblock %} + diff --git a/templates/mahlzeiten/_mahlzeit.html.twig b/templates/mahlzeiten/_mahlzeit.html.twig index ab4ea54..e9a9cd2 100644 --- a/templates/mahlzeiten/_mahlzeit.html.twig +++ b/templates/mahlzeiten/_mahlzeit.html.twig @@ -3,49 +3,98 @@ eingebunden und als htmx-Fragment von den CheckOff/Uncheck-Endpunkten zurückgeliefert (htmx ersetzt per hx-swap="outerHTML" genau diese
  • ). - @param MahlzeitResponse m - @param bool sortierbar optional: zeigt Drag-Handle + data-id (Planungsliste) -#} -
  • -
    +
    {% if sortierbar is defined and sortierbar %} - ⋮ + + + + + {% endif %} - {{ m.name }} - {% if m.notiz != '' %} -
    {{ m.notiz }}
    - {% endif %} - {% if m.wochentage|length > 0 %} -
    - {% for tag in m.wochentage %} - {{ tag.name }} - {% endfor %} -
    - {% endif %} - - {{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }} - {% if m.status.value == 'gegessen' and m.gegessenAm %} - · {{ m.gegessenAm|date('d.m.Y H:i') }} +
    + {{ m.name }} + {% if m.notiz != '' %} +
    {{ m.notiz }}
    {% endif %} - + {% if m.wochentage|length > 0 %} +
    + {% for tag in m.wochentage %} + {{ tag.name }} + {% endfor %} +
    + {% endif %} + + {{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }} + {% if m.status.value == 'gegessen' and m.gegessenAm %} + · {{ m.gegessenAm|date('d.m.Y H:i') }} + {% endif %} + +
    - {% if m.status.value == 'geplant' %} - - {% else %} - - {% endif %} - Bearbeiten -
    - -
    +
    + {% if m.status.value == 'geplant' %} + + {% else %} + + {% endif %} +
    +
    + + + Bearbeiten + +
    + +
    +
  • diff --git a/templates/mahlzeiten/archiv.html.twig b/templates/mahlzeiten/archiv.html.twig index f650cce..c776aaa 100644 --- a/templates/mahlzeiten/archiv.html.twig +++ b/templates/mahlzeiten/archiv.html.twig @@ -4,18 +4,19 @@ {% block body %}
    -
    -

    Archiv

    +
    +
    +

    Archiv

    +
    Alle abgehakten (gegessenen) Mahlzeiten. „Aufheben" stellt sie wieder auf „geplant" um.
    +
    -

    Alle abgehakten (gegessenen) Mahlzeiten. „Aufheben" stellt sie wieder auf „geplant" um.

    - {% if mahlzeiten|length == 0 %} -

    Noch nichts gegessen – das Archiv ist leer.

    +

    Noch nichts gegessen – das Archiv ist leer.

    {% else %}