diff --git a/Procfile b/Procfile index 700057e..92aba31 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bin/scalingo_run_web worker: celery -A calendars.celery_app worker --task-events --beat -l INFO -c $DJANGO_CELERY_CONCURRENCY -Q celery,default -postdeploy: python manage.py migrate +postdeploy: source bin/export_pg_vars.sh && python manage.py migrate && SQL_DIR=/app/sabredav/sql bash sabredav/init-database.sh diff --git a/bin/export_pg_vars.sh b/bin/export_pg_vars.sh new file mode 100755 index 0000000..7988acc --- /dev/null +++ b/bin/export_pg_vars.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Parse DATABASE_URL into individual PG* environment variables. +# Usage: source bin/export_pg_vars.sh +# +# Needed because PHP (server.php) and psql (init-database.sh) expect +# PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD — but Scalingo only +# provides DATABASE_URL. + +if [ -n "$DATABASE_URL" ] && [ -z "$PGHOST" ]; then + eval "$(python3 -c " +import os, urllib.parse +u = urllib.parse.urlparse(os.environ['DATABASE_URL']) +print(f'export PGHOST=\"{u.hostname}\"') +print(f'export PGPORT=\"{u.port or 5432}\"') +print(f'export PGDATABASE=\"{u.path.lstrip(\"/\")}\"') +print(f'export PGUSER=\"{u.username}\"') +print(f'export PGPASSWORD=\"{urllib.parse.unquote(u.password)}\"') +")" + echo "-----> Parsed DATABASE_URL into PG* vars (host=$PGHOST port=$PGPORT db=$PGDATABASE)" +fi diff --git a/bin/scalingo_postfrontend b/bin/scalingo_postfrontend index 6036678..8df2741 100644 --- a/bin/scalingo_postfrontend +++ b/bin/scalingo_postfrontend @@ -13,3 +13,46 @@ mv src/backend/* ./ mv src/nginx/* ./ echo "3.13" > .python-version + +# --- PHP + SabreDAV setup --- +echo "-----> Installing PHP 8.3 from Ubuntu packages" + +PHP_PREFIX=".php" +DEB_DIR="/tmp/php-debs" +mkdir -p "$DEB_DIR" "$PHP_PREFIX" + +BASE_URL="http://security.ubuntu.com/ubuntu/pool/main/p/php8.3" +VERSION="8.3.6-0ubuntu0.24.04.6" + +for pkg in cli fpm common opcache readline pgsql xml mbstring curl; do + echo " Downloading php8.3-${pkg}" + curl -fsSL -o "$DEB_DIR/php8.3-${pkg}.deb" \ + "${BASE_URL}/php8.3-${pkg}_${VERSION}_amd64.deb" +done +curl -fsSL -o "$DEB_DIR/php-common.deb" \ + "http://mirrors.kernel.org/ubuntu/pool/main/p/php-defaults/php-common_93ubuntu2_all.deb" + +for deb in "$DEB_DIR"/*.deb; do + dpkg-deb -x "$deb" "$PHP_PREFIX" +done + +# Create php wrapper +cat > bin/php << 'WRAPPER' +#!/bin/bash +DIR="$(cd "$(dirname "$0")/.." && pwd)" +PHP_INI_SCAN_DIR="$DIR/.php/etc/php/8.3/cli/conf.d" \ + exec "$DIR/.php/usr/bin/php8.3" "$@" +WRAPPER +chmod +x bin/php + +echo "-----> PHP version: $(bin/php -v | head -1)" + +# Download Composer and install SabreDAV dependencies +echo "-----> Installing SabreDAV dependencies" +curl -fsSL -o bin/composer.phar \ + https://getcomposer.org/download/latest-stable/composer.phar +cp -r docker/sabredav sabredav +cd sabredav +../bin/php ../bin/composer.phar install \ + --no-dev --optimize-autoloader --no-interaction +cd .. diff --git a/bin/scalingo_run_web b/bin/scalingo_run_web index cac67f4..c33ff5f 100644 --- a/bin/scalingo_run_web +++ b/bin/scalingo_run_web @@ -1,5 +1,14 @@ #!/bin/bash +# Parse DATABASE_URL into PG* vars for PHP and psql +source bin/export_pg_vars.sh + +# Start PHP-FPM for SabreDAV (CalDAV server) +PHP_INI_SCAN_DIR=/app/.php/etc/php/8.3/cli/conf.d \ + .php/usr/sbin/php-fpm8.3 \ + --fpm-config /app/sabredav/php-fpm.conf \ + --nodaemonize & + # Start the Django backend gunicorn -b :8000 calendars.wsgi:application --log-file - & diff --git a/docker/sabredav/init-database.sh b/docker/sabredav/init-database.sh index f421470..9e87e73 100644 --- a/docker/sabredav/init-database.sh +++ b/docker/sabredav/init-database.sh @@ -40,8 +40,8 @@ done echo "PostgreSQL is ready. Initializing sabre/dav database schema..." -# SQL files directory (will be copied into container) -SQL_DIR="/var/www/sabredav/sql" +# SQL files directory (configurable for Scalingo, defaults to Docker path) +SQL_DIR="${SQL_DIR:-/var/www/sabredav/sql}" # Check if tables already exist TABLES_EXIST=$(psql -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('users', 'principals', 'calendars')" 2>/dev/null || echo "0") diff --git a/docker/sabredav/php-fpm.conf b/docker/sabredav/php-fpm.conf new file mode 100644 index 0000000..1aaf26a --- /dev/null +++ b/docker/sabredav/php-fpm.conf @@ -0,0 +1,25 @@ +[global] +daemonize = no +error_log = /dev/stderr +pid = /tmp/php-fpm.pid + +[www] +listen = /tmp/php-fpm.sock +listen.mode = 0666 + +; When running as non-root, user/group settings are ignored +user = www-data +group = www-data + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +; Pass all env vars to PHP workers (for PGHOST, CALDAV_* keys, etc.) +clear_env = no + +; Logging +catch_workers_output = yes +decorate_workers_output = no diff --git a/src/frontend/apps/calendars/src/features/ui/hooks/useDynamicFavicon.ts b/src/frontend/apps/calendars/src/features/ui/hooks/useDynamicFavicon.ts new file mode 100644 index 0000000..1718406 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/ui/hooks/useDynamicFavicon.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; + +const FAVICON_SIZE = 64; + +const CALENDAR_SVG = ` + + +`; + +function generateFaviconDataUrl(day: number): Promise { + return new Promise((resolve, reject) => { + const canvas = document.createElement("canvas"); + canvas.width = FAVICON_SIZE; + canvas.height = FAVICON_SIZE; + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("Canvas 2D context not available")); + return; + } + + const img = new Image(); + const svgBase64 = btoa(CALENDAR_SVG); + img.src = `data:image/svg+xml;base64,${svgBase64}`; + + img.onload = () => { + ctx.drawImage(img, 0, 0, FAVICON_SIZE, FAVICON_SIZE); + + const text = String(day); + const fontSize = day >= 10 ? 26 : 28; + ctx.font = `bold ${fontSize}px system-ui, -apple-system, sans-serif`; + ctx.fillStyle = "rgba(247, 248, 248, 0.95)"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(text, FAVICON_SIZE / 2, FAVICON_SIZE * 0.6); + + resolve(canvas.toDataURL("image/png")); + }; + + img.onerror = () => reject(new Error("Failed to load SVG into Image")); + }); +} + +/** + * Returns a data URL for a favicon with the current day of month, + * or null while generating. Use this in the Next.js link tag + * so React manages the DOM and doesn't overwrite it. + */ +export function useDynamicFavicon(): string | null { + const [faviconUrl, setFaviconUrl] = useState(null); + + useEffect(() => { + const day = new Date().getDate(); + generateFaviconDataUrl(day).then(setFaviconUrl); + }, []); + + return faviconUrl; +} diff --git a/src/frontend/apps/calendars/src/pages/_app.tsx b/src/frontend/apps/calendars/src/pages/_app.tsx index 95a5508..451bbc9 100644 --- a/src/frontend/apps/calendars/src/pages/_app.tsx +++ b/src/frontend/apps/calendars/src/pages/_app.tsx @@ -32,6 +32,7 @@ import { useCunninghamTheme, } from "@/features/ui/cunningham/useCunninghamTheme"; import { FeedbackFooterMobile } from "@/features/feedback/Feedback"; +import { useDynamicFavicon } from "@/features/ui/hooks/useDynamicFavicon"; export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; @@ -115,16 +116,15 @@ const MyAppInner = ({ Component, pageProps }: AppPropsWithLayout) => { const { t, i18n } = useTranslation(); const { theme } = useAppContext(); const themeTokens = useCunninghamTheme(); + const dynamicFavicon = useDynamicFavicon(); + const faviconHref = + dynamicFavicon || removeQuotes(themeTokens.components.favicon.src); return ( <> {t("app_title")} - +