✨(deploy) add Scalingo buildpack + dynamic favicon
This commit is contained in:
2
Procfile
2
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
|
||||
|
||||
20
bin/export_pg_vars.sh
Executable file
20
bin/export_pg_vars.sh
Executable file
@@ -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
|
||||
@@ -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 ..
|
||||
|
||||
@@ -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 - &
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
25
docker/sabredav/php-fpm.conf
Normal file
25
docker/sabredav/php-fpm.conf
Normal file
@@ -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
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const FAVICON_SIZE = 64;
|
||||
|
||||
const CALENDAR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M8.18213 11.5846C8.59434 11.4599 9.03364 11.3924 9.49206 11.3924H30.5085C30.9669 11.3924 31.4062 11.4599 31.8184 11.5846C31.8178 10.391 31.8053 9.77042 31.5608 9.29057C31.3342 8.84582 30.9726 8.48423 30.5279 8.25762C30.0223 8 29.3604 8 28.0366 8H11.9639C10.6402 8 9.97827 8 9.47266 8.25762C9.02792 8.48423 8.66633 8.84582 8.43972 9.29057C8.19522 9.77041 8.18276 10.391 8.18213 11.5846Z" fill="#FBC63A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.3585 22.7595L32.9244 16.496C33.3174 14.9241 32.1285 13.4014 30.5082 13.4014H9.49178C7.8715 13.4014 6.68261 14.9241 7.07559 16.496L8.64148 22.7595C8.74063 23.1561 8.74063 23.571 8.64148 23.9676L7.07559 30.2312C6.68261 31.8031 7.8715 33.3258 9.49178 33.3258H30.5082C32.1285 33.3258 33.3174 31.8031 32.9244 30.2312L31.3585 23.9676C31.2594 23.571 31.2594 23.1561 31.3585 22.7595Z" fill="#2845C1"/>
|
||||
</svg>`;
|
||||
|
||||
function generateFaviconDataUrl(day: number): Promise<string> {
|
||||
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 <Head> link tag
|
||||
* so React manages the DOM and doesn't overwrite it.
|
||||
*/
|
||||
export function useDynamicFavicon(): string | null {
|
||||
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const day = new Date().getDate();
|
||||
generateFaviconDataUrl(day).then(setFaviconUrl);
|
||||
}, []);
|
||||
|
||||
return faviconUrl;
|
||||
}
|
||||
@@ -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<P = object, IP = P> = NextPage<P, IP> & {
|
||||
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 (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("app_title")}</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href={removeQuotes(themeTokens.components.favicon.src)}
|
||||
type="image/png"
|
||||
/>
|
||||
<link rel="icon" href={faviconHref} type="image/png" />
|
||||
</Head>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider
|
||||
|
||||
@@ -52,3 +52,17 @@ server {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# Internal CalDAV server (SabreDAV via PHP-FPM)
|
||||
# Only accessible from localhost — Django proxies to this
|
||||
server {
|
||||
listen 127.0.0.1:9001;
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/tmp/php-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME /app/sabredav/server.php;
|
||||
fastcgi_read_timeout 60;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user