Remove static JSON files and seed step; migrate site settings to Postgres

All content (recipes + imprint/privacy settings) now lives exclusively in
Postgres. The app no longer requires a seed step on first deploy.

Changes:
- helpers.php: load_site_settings() returns defaults when site_settings
  table is empty instead of throwing DatabaseUnavailableException; removes
  legacy JSONB migration path
- data/recipes.json, data/site.json: deleted (content already in DB)
- scripts/db-seed.php: deleted (no longer needed)
- docker-compose.yml: remove RUN_DB_SEED env var and data/ volume mount
- docker/entrypoint.sh: remove RUN_DB_SEED seed block
- docs/COOLIFY.md: update deployment guide to reflect seedless workflow
- .claude/launch.json: add dev server configurations for preview tool

Fresh deploys now start with placeholder legal pages and an empty recipe
list; the admin fills in real content via /admin.php without any CLI step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 09:53:39 +02:00
co-authored by Claude Sonnet 4.6
parent 792cbe18c1
commit 86fbd8e4bb
20 changed files with 154 additions and 512 deletions
+60 -65
View File
@@ -69,61 +69,12 @@ function recipe_base_from_row(array $row): array {
];
}
function decode_legacy_jsonb_value($value): ?array {
if (is_array($value)) {
return $value;
}
if (is_string($value)) {
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : null;
}
if ($value !== null) {
$decoded = json_decode(json_encode($value), true);
return is_array($decoded) ? $decoded : null;
}
return null;
}
function migrate_legacy_jsonb_storage(PDO $pdo): void {
if (!db_table_has_column($pdo, 'recipes', 'data')) {
return;
}
$legacy = [];
$stmt = $pdo->query('SELECT slug, data FROM recipes');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$recipe = decode_legacy_jsonb_value($row['data']);
if (is_array($recipe) && !empty($recipe['slug'])) {
$legacy[] = $recipe;
}
}
$pdo->exec('DROP TABLE IF EXISTS recipe_steps CASCADE');
$pdo->exec('DROP TABLE IF EXISTS recipe_utensils CASCADE');
$pdo->exec('DROP TABLE IF EXISTS recipe_ingredients CASCADE');
$pdo->exec('DROP TABLE IF EXISTS recipe_tags CASCADE');
$pdo->exec('DROP TABLE IF EXISTS recipe_translations CASCADE');
$pdo->exec('DROP TABLE IF EXISTS recipes CASCADE');
apply_recipe_schema($pdo);
foreach ($legacy as $recipe) {
save_recipe($recipe, $pdo);
}
}
function ensure_recipe_schema(PDO $pdo): void {
static $ready = false;
if ($ready) {
return;
}
if (db_table_has_column($pdo, 'recipes', 'data') && !db_table_has_column($pdo, 'recipes', 'calories')) {
migrate_legacy_jsonb_storage($pdo);
$ready = true;
return;
}
apply_recipe_schema($pdo);
$ready = true;
}
@@ -446,29 +397,73 @@ function default_site_settings(): array {
}
/**
* Load site-wide settings for legal pages, merging with defaults.
* Load site-wide settings for legal pages from Postgres.
*/
function load_site_settings(): array {
$defaults = default_site_settings();
$path = __DIR__ . '/data/site.json';
if (!file_exists($path)) {
return $defaults;
function load_site_settings(bool $refresh = false): array {
static $cache = null;
if ($cache !== null && !$refresh) {
return $cache;
}
$raw = file_get_contents($path);
$data = json_decode($raw, true);
if (!is_array($data)) {
return $defaults;
$settings = default_site_settings();
$pdo = require_database();
$stmt = $pdo->query(
'SELECT section, lang, setting_key, setting_value FROM site_settings ORDER BY section, lang, setting_key'
);
$hasRows = false;
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$section = $row['section'];
$lang = $row['lang'];
$key = $row['setting_key'];
if (!isset($settings[$section][$lang])) {
continue;
}
$settings[$section][$lang][$key] = $key === 'log_retention_days'
? (int) $row['setting_value']
: $row['setting_value'];
$hasRows = true;
}
return array_replace_recursive($defaults, $data);
$cache = $settings;
return $cache;
}
/**
* Persist site-wide settings for legal pages.
* Persist site-wide settings for legal pages in Postgres.
*/
function save_site_settings(array $settings): bool {
$path = __DIR__ . '/data/site.json';
$json = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return (bool) file_put_contents($path, $json);
function save_site_settings(array $settings, ?PDO $pdo = null): bool {
$pdo = $pdo ?? require_database();
try {
$pdo->beginTransaction();
$pdo->exec("DELETE FROM site_settings WHERE section IN ('imprint', 'privacy')");
$stmt = $pdo->prepare(
'INSERT INTO site_settings (section, lang, setting_key, setting_value, updated_at)
VALUES (?, ?, ?, ?, NOW())
ON CONFLICT (section, lang, setting_key) DO UPDATE SET
setting_value = EXCLUDED.setting_value,
updated_at = NOW()'
);
foreach (['imprint', 'privacy'] as $section) {
foreach (['de', 'en'] as $lang) {
foreach (($settings[$section][$lang] ?? []) as $key => $value) {
$stmt->execute([$section, $lang, $key, (string) $value]);
}
}
}
$pdo->commit();
return true;
} catch (\Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('save_site_settings failed: ' . $e->getMessage());
return false;
}
}
function slugify(string $text): string {