#!/bin/sh set -e DATA_DIR=/app/data SEED_DIR=/app/data-seed # Ensure the data directory exists (in case the volume was not mounted) mkdir -p "$DATA_DIR" # Seed each default JSON file only when missing. # This runs on first boot or a fresh volume, but never overwrites existing data. if [ -d "$SEED_DIR" ]; then for seed_file in "$SEED_DIR"/*.json; do [ -f "$seed_file" ] || continue base_name=$(basename "$seed_file") target_file="$DATA_DIR/$base_name" if [ ! -f "$target_file" ]; then echo "[siteforge] No $base_name found. Seeding from image defaults..." cp "$seed_file" "$target_file" fi done fi # Merge newly added seeded platform links into existing admin content on upgrades # without overwriting user-edited values already present in the live volume. merge_seed_platform_links() { target_file="$1" seed_file="$2" [ -f "$target_file" ] || return 0 [ -f "$seed_file" ] || return 0 node - "$target_file" "$seed_file" <<'NODE' const fs = require('fs') const [targetFile, seedFile] = process.argv.slice(2) function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')) } function normalizeLinks(value) { return Array.isArray(value) ? value.filter(item => item && typeof item === 'object') : [] } const target = readJson(targetFile) const seed = readJson(seedFile) if (!target.siteContent || !seed.siteContent) process.exit(0) const targetLinks = normalizeLinks(target.siteContent.customLinks) const seedLinks = normalizeLinks(seed.siteContent.customLinks) let changed = false for (const seedLink of seedLinks) { if (seedLink.placement !== 'platforms' || !seedLink.id || !seedLink.label || !seedLink.url) continue const existingIndex = targetLinks.findIndex(link => link.id === seedLink.id) if (existingIndex === -1) { targetLinks.push(seedLink) changed = true continue } const existing = targetLinks[existingIndex] const merged = { ...existing } if (!merged.imageUrl && seedLink.imageUrl) { merged.imageUrl = seedLink.imageUrl changed = true } if (!merged.placement && seedLink.placement) { merged.placement = seedLink.placement changed = true } if (!merged.label && seedLink.label) { merged.label = seedLink.label changed = true } if (!merged.url && seedLink.url) { merged.url = seedLink.url changed = true } targetLinks[existingIndex] = merged } if (!changed) process.exit(0) target.siteContent.customLinks = targetLinks fs.writeFileSync(targetFile, `${JSON.stringify(target, null, 2)}\n`) console.log(`[siteforge] Merged seeded platform links into ${targetFile}`) NODE } merge_seed_platform_links "$DATA_DIR/admin-content.json" "$SEED_DIR/admin-content.json" merge_seed_platform_links "$DATA_DIR/admin-content-draft.json" "$SEED_DIR/admin-content-draft.json" # Backfill missing lesson releasedAt values from seeded defaults on upgrades. # This keeps persistent volumes compatible with release-date access gating. merge_seed_study_release_dates() { target_file="$1" seed_file="$2" [ -f "$target_file" ] || return 0 [ -f "$seed_file" ] || return 0 node - "$target_file" "$seed_file" <<'NODE' const fs = require('fs') const [targetFile, seedFile] = process.argv.slice(2) function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')) } function asArray(value) { return Array.isArray(value) ? value : [] } function mergeStudyDates(targetStudies, seedStudies) { let changed = false const seedBySlug = new Map() for (const seedStudy of asArray(seedStudies)) { if (!seedStudy || typeof seedStudy !== 'object') continue if (typeof seedStudy.slug !== 'string' || !seedStudy.slug) continue seedBySlug.set(seedStudy.slug, seedStudy) } for (const targetStudy of asArray(targetStudies)) { if (!targetStudy || typeof targetStudy !== 'object') continue if (typeof targetStudy.slug !== 'string' || !targetStudy.slug) continue const seedStudy = seedBySlug.get(targetStudy.slug) if (!seedStudy) continue const seedSectionById = new Map() for (const seedSection of asArray(seedStudy.sections)) { if (!seedSection || typeof seedSection !== 'object') continue if (typeof seedSection.id !== 'string' || !seedSection.id) continue seedSectionById.set(seedSection.id, seedSection) } for (const targetSection of asArray(targetStudy.sections)) { if (!targetSection || typeof targetSection !== 'object') continue if (typeof targetSection.id !== 'string' || !targetSection.id) continue if (typeof targetSection.releasedAt === 'string' && targetSection.releasedAt.trim()) continue const seedSection = seedSectionById.get(targetSection.id) if (!seedSection || typeof seedSection.releasedAt !== 'string' || !seedSection.releasedAt.trim()) continue targetSection.releasedAt = seedSection.releasedAt changed = true } } return changed } function mergeLegacyColossiansDates(targetSections, seedSections) { let changed = false const seedById = new Map() for (const seedSection of asArray(seedSections)) { if (!seedSection || typeof seedSection !== 'object') continue if (typeof seedSection.id !== 'string' || !seedSection.id) continue seedById.set(seedSection.id, seedSection) } for (const targetSection of asArray(targetSections)) { if (!targetSection || typeof targetSection !== 'object') continue if (typeof targetSection.id !== 'string' || !targetSection.id) continue if (typeof targetSection.releasedAt === 'string' && targetSection.releasedAt.trim()) continue const seedSection = seedById.get(targetSection.id) if (!seedSection || typeof seedSection.releasedAt !== 'string' || !seedSection.releasedAt.trim()) continue targetSection.releasedAt = seedSection.releasedAt changed = true } return changed } const target = readJson(targetFile) const seed = readJson(seedFile) if (!target.siteContent || !seed.siteContent) process.exit(0) const targetContent = target.siteContent const seedContent = seed.siteContent const changedStudies = mergeStudyDates(targetContent.studies, seedContent.studies) const changedLegacy = mergeLegacyColossiansDates(targetContent.colossiansStudySections, seedContent.colossiansStudySections) if (!changedStudies && !changedLegacy) process.exit(0) fs.writeFileSync(targetFile, `${JSON.stringify(target, null, 2)}\n`) console.log(`[siteforge] Backfilled missing releasedAt values in ${targetFile}`) NODE } merge_seed_study_release_dates "$DATA_DIR/admin-content.json" "$SEED_DIR/admin-content.json" merge_seed_study_release_dates "$DATA_DIR/admin-content-draft.json" "$SEED_DIR/admin-content-draft.json" exec node server.js