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:
+60
-65
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user