Retire the one-time seed script #9

Merged
ngu-git-admin merged 1 commit from chore/retire-seed into main 2026-09-26 22:21:21 +01:00
3 changed files with 0 additions and 696 deletions
Showing only changes of commit 370fce6e9a - Show all commits

View file

@ -21,7 +21,6 @@ Frontend (repo root):
Backend (`server/`, Node >= 22). The server reads `HOST` (default `127.0.0.1`), `PORT` (default `3001`) and `DB_PATH` (default `./ngu.db`); locally, use `DB_PATH=./dev.db`:
- `DB_PATH=./dev.db pnpm dev`: run with `node --watch`
- `DB_PATH=./dev.db pnpm migrate`: apply migrations without starting the server
- `DB_PATH=./dev.db node src/seed.js`: rebuild content tables from `src/data/`. It wipes every content table first (feedback is kept). Run it from the repo, not the deployed copy.
- `DB_PATH=./dev.db node src/admin-cli.js add|list|passwd|role|disable|enable ...`: the only way accounts are created
In dev, Vite proxies `/api` to the target set in `vite.config.ts`, so the API must listen on that port.

View file

@ -1,398 +0,0 @@
/* ═══════════════════════════════════════════════════════════════
SEED
Reads the two static data modules and fills the database from
them. Run once to make the move, and re-runnable after you tweak
the source files.
cd /root/NGU-Web.v1.3-sqlite/server
DB_PATH=./dev.db node src/seed.js
Run it from the repo, not from /srv/ngu-api — the deployed copy
has no src/data to read.
⚠ It clears every content table first, so anything typed
straight into the database is lost. Feedback is never touched.
Section presentation (title, accent, background, defaultView) is
NOT imported. Retreats.jsx owns that; only the ids come across,
so section_id has something real to reference.
Three things it deliberately does NOT do, each flagged in the
warnings at the end rather than guessed at:
dates "March/April 2026" isn't parseable, and half-right
dates are worse than none. starts_on stays null and
the explicit status carries the upcoming/past split
exactly as it does today.
partners the five partner events are placeholders with no
organization behind them, so host_org_id is null.
leads "Chapter lead name" is not a person. Inventing a
people row from a placeholder string would put a
fake name on the site.
═══════════════════════════════════════════════════════════════ */
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { openDatabase, migrate, tx } from "./db.js";
const HERE = dirname(fileURLToPath(import.meta.url));
const DB_PATH = process.env.DB_PATH ?? "./dev.db";
const EVENTS_MODULE = process.env.EVENTS_MODULE ?? "../../src/data/events.ts";
const CHAPTERS_MODULE = process.env.CHAPTERS_MODULE ?? "../../src/data/chapters.ts";
// The root organization. Every national retreat hangs off this, and
// it's what makes the org_logo fallback work uniformly.
const NGU = {
id: "ngu",
name: "Next Generation of Unity",
short_name: "NGU",
color: "#138ba0",
logo: "ngu-logo-white-bg.svg",
};
const warnings = [];
const warn = (message) => warnings.push(message);
/* ── Load the source modules ───────────────────────────────── */
async function load(relative) {
const path = resolve(HERE, relative);
try {
return await import(pathToFileURL(path).href);
} catch (err) {
console.error(`\nCould not read ${path}`);
console.error("Set EVENTS_MODULE / CHAPTERS_MODULE if they live elsewhere.\n");
throw err;
}
}
const eventsModule = await load(EVENTS_MODULE);
const chaptersModule = await load(CHAPTERS_MODULE);
const eventsData = eventsModule.default;
const { GROUPS, SPLITS, CHAPTERS, STATE_NAMES, groupOf } = chaptersModule;
/* ── Helpers ───────────────────────────────────────────────── */
const isStateCode = (code) =>
Boolean(code) && code !== "CANADA" && code in STATE_NAMES;
const opposite = (edge) => (edge === "top" ? "bottom" : "top");
const instagramUrl = (handle) =>
`https://instagram.com/${String(handle).replace(/^@/, "")}`;
// "Unity Village, MO" → { locality, state_code }. Anything that
// doesn't end in a real state code keeps the whole string as the
// locality, and location_label carries the original either way.
function splitPlace(label) {
if (!label) return { locality: null, state_code: null };
const comma = label.lastIndexOf(",");
if (comma === -1) return { locality: label.trim(), state_code: null };
const head = label.slice(0, comma).trim();
const tail = label.slice(comma + 1).trim();
return isStateCode(tail)
? { locality: head, state_code: tail }
: { locality: label.trim(), state_code: null };
}
function chapterLocation(chapter) {
const online = chapter.state === null && !chapter.city?.includes(",");
if (online || /^online$/i.test(chapter.city ?? "")) {
return {
locality: null, state_code: null, country: "US",
location_label: chapter.city ?? "Online", is_online: 1,
};
}
if (chapter.state === "CANADA") {
return {
locality: splitPlace(chapter.city).locality,
state_code: null, country: "CA",
location_label: chapter.city, is_online: 0,
};
}
const { locality } = splitPlace(chapter.city);
return {
locality,
state_code: isStateCode(chapter.state) ? chapter.state : null,
country: "US",
location_label: chapter.city,
is_online: 0,
};
}
function eventLocation(label) {
if (!label || /^online$/i.test(label)) {
return {
locality: null, state_code: null, country: "US",
location_label: label ?? null, is_online: label ? 1 : 0,
};
}
const { locality, state_code } = splitPlace(label);
return { locality, state_code, country: "US", location_label: label, is_online: 0 };
}
/* ── Open ──────────────────────────────────────────────────── */
const db = await openDatabase(DB_PATH);
migrate(db, { log: () => {} });
const version = db.prepare("PRAGMA user_version").get().user_version;
if (version < 2) {
throw new Error(`Schema is at v${version}; seed needs v2. Check 002_schema.sql.`);
}
/* ── Statements ────────────────────────────────────────────── */
const ins = {
org: db.prepare(`
INSERT INTO organizations
(id, kind, name, short_name, tagline, color, logo,
venue, locality, state_code, country, location_label, is_online,
is_published, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`),
region: db.prepare(`INSERT INTO regions (id, scope, map_note) VALUES (?, ?, ?)`),
regionArea: db.prepare(`
INSERT INTO region_areas (region_id, area_code, share, edge, note)
VALUES (?, ?, ?, ?, ?)`),
chapter: db.prepare(`
INSERT INTO chapters (id, region_id, meets, started) VALUES (?, ?, ?, ?)`),
section: db.prepare(`
INSERT INTO event_sections (id, name, sort_order) VALUES (?, ?, ?)`),
event: db.prepare(`
INSERT INTO events
(id, section_id, host_org_id, title, theme,
date_label, status,
locality, state_code, country, location_label, is_online,
org_logo, event_logo, color, gradient, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
block: db.prepare(`
INSERT INTO content_blocks (owner_kind, owner_id, slot, sort_order, type, text)
VALUES (?, ?, ?, ?, ?, ?)`),
link: db.prepare(`
INSERT INTO links (owner_kind, owner_id, sort_order, kind, platform, label, url, is_primary)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
};
const addParagraph = (kind, id, slot, order, text) => {
if (!text) return;
ins.block.run(kind, id, slot, order, "paragraph", text);
};
/* ── Clear ─────────────────────────────────────────────────────
Children before parents. Feedback is not in this list and is
never cleared.
───────────────────────────────────────────────────────────── */
const CLEAR = [
"people_list_members", "people_lists",
"person_awards", "awards",
"event_people", "affiliations", "teams",
"person_private", "people",
"content_block_items", "content_blocks", "links",
"events", "event_sections",
"chapters", "region_areas", "regions", "organizations",
];
/* ── Import ────────────────────────────────────────────────── */
const counts = {};
const bump = (key, n = 1) => (counts[key] = (counts[key] ?? 0) + n);
tx(db, () => {
for (const table of CLEAR) db.exec(`DELETE FROM ${table}`);
db.exec("DELETE FROM sqlite_sequence");
/* ── The root organization ───────────────────────────────── */
ins.org.run(
NGU.id, "national", NGU.name, NGU.short_name, null, NGU.color, NGU.logo,
null, null, null, "US", null, 0, 0,
);
bump("organizations");
/* ── Regions ─────────────────────────────────────────────── */
GROUPS.forEach((group, index) => {
ins.org.run(
group.id, "region", group.name, null, null, group.color, null,
null, null, null, "US", null, 0, index,
);
ins.region.run(group.id, group.scope, group.note ?? null);
bump("organizations");
bump("regions");
// Whole areas. Split states are skipped here and handled below,
// which matters for Iowa — it appears in great-lakes.states AND
// in SPLITS, and inserting it twice would violate the key.
for (const area of group.states) {
if (SPLITS[area]) continue;
ins.regionArea.run(group.id, area, 1.0, null, null);
bump("region_areas");
}
});
// Shared areas, one row per region. The old SPLITS gave the
// sliver an explicit share and left the primary implicit; both
// are explicit now, so the renderer never subtracts.
for (const [area, split] of Object.entries(SPLITS)) {
ins.regionArea.run(
split.primary, area,
Number((1 - split.share).toFixed(4)),
opposite(split.edge),
split.primaryNote ?? null,
);
ins.regionArea.run(
split.secondary, area, split.share, split.edge, split.secondaryNote ?? null,
);
bump("region_areas", 2);
}
/* ── Chapters ────────────────────────────────────────────── */
CHAPTERS.forEach((chapter, index) => {
const place = chapterLocation(chapter);
const region = groupOf(chapter);
if (!region) warn(`Chapter "${chapter.id}" resolved to no region.`);
ins.org.run(
chapter.id, "chapter", chapter.name, null, null, null, chapter.logo ?? null,
chapter.where ?? null,
place.locality, place.state_code, place.country,
place.location_label, place.is_online,
index,
);
ins.chapter.run(
chapter.id, region?.id ?? null, chapter.meets ?? null, chapter.started ?? null,
);
bump("organizations");
bump("chapters");
addParagraph("organization", chapter.id, "body", 0, chapter.about);
let order = 0;
if (chapter.link) {
ins.link.run("organization", chapter.id, order++, "website", null, "Visit", chapter.link, 1);
bump("links");
}
if (chapter.contact) {
ins.link.run(
"organization", chapter.id, order++, "email", null,
chapter.contact, `mailto:${chapter.contact}`, 0,
);
bump("links");
}
if (chapter.leads) {
warn(`Chapter "${chapter.id}" has leads "${chapter.leads}" — add a people row and an affiliation.`);
}
});
/* ── Event sections ────────────────────────────────────────
Ids only. Titles, accents, colours, backgrounds and default
views stay in Retreats.jsx.
─────────────────────────────────────────────────────────── */
eventsData.sections.forEach((section, index) => {
ins.section.run(section.id, section.title, index);
bump("event_sections");
});
/* ── Events ──────────────────────────────────────────────── */
const regionIds = new Set(GROUPS.map((g) => g.id));
// National retreats belong to NGU. Regional ones name their region
// in the slug ("northwest-2026"). Partner placeholders have no
// organization yet.
function hostFor(event, sectionId) {
if (sectionId === "national") return NGU.id;
if (sectionId === "regional") {
const match = [...regionIds]
.filter((id) => event.id.startsWith(`${id}-`))
.sort((a, b) => b.length - a.length)[0];
if (match) return match;
warn(`Event "${event.id}" is regional but names no region — host left null.`);
return null;
}
warn(`Event "${event.id}" has no partner organization — host left null.`);
return null;
}
for (const section of eventsData.sections) {
section.events.forEach((event, index) => {
const place = eventLocation(event.location);
ins.event.run(
event.id, section.id, hostFor(event, section.id),
event.title, event.theme ?? null,
event.date ?? null, event.status ?? null,
place.locality, place.state_code, place.country,
place.location_label, place.is_online,
event.org_logo ?? null, event.image ?? null,
event.color ?? null, event.gradient ?? null,
index,
);
bump("events");
// desc_a and desc_b become the card slot, in order. The body
// slot is left empty for the full page you'll write later.
addParagraph("event", event.id, "card", 0, event.desc_a);
addParagraph("event", event.id, "card", 1, event.desc_b);
let order = 0;
(event.links ?? []).forEach((link, i) => {
if (!/^https?:\/\//.test(link.link)) {
warn(`Event "${event.id}" link "${link.label}" is not a URL: ${link.link}`);
}
ins.link.run(
"event", event.id, order++, "action", null,
link.label, link.link, i === 0 ? 1 : 0,
);
bump("links");
});
if (event.instagram) {
ins.link.run(
"event", event.id, order++, "social", "instagram",
event.instagram, instagramUrl(event.instagram), 0,
);
bump("links");
}
});
}
});
db.close();
/* ── Report ────────────────────────────────────────────────── */
console.log(`\nSeeded ${DB_PATH}\n`);
for (const [table, n] of Object.entries(counts).sort()) {
console.log(` ${String(n).padStart(4)} ${table}`);
}
if (warnings.length > 0) {
console.log(`\n${warnings.length} thing${warnings.length === 1 ? "" : "s"} to follow up:\n`);
for (const message of warnings) console.log(` · ${message}`);
}
console.log("");

View file

@ -1,297 +0,0 @@
/* ═══════════════════════════════════════════════════════════════
EVENT DATA
Plain JS instead of JSON so it imports cleanly in any environment
(Figma included) — same shape, but comments and trailing commas
are allowed.
Per event:
org_logo hosting org's logo: a filename WITH extension in
public/event-logos/, e.g. "ngu-logo-white-bg.png".
null = fall back to DEFAULT_ORG_LOGO.
image event logo / flyer, same filename rule. A name with
no matching file just renders nothing.
instagram handle for the follow button, e.g. "@nextgenunity".
null = no Instagram button on this card.
color card outline / button color. null = the section's
defaultColor.
gradient card background. null = no gradient.
desc_a first paragraph
desc_b second paragraph (pricing, lodging, a note — anything)
status "upcoming" or "past" — grid view splits on this.
links [] when there's nothing to click yet.
Per section:
accent heading, banner, arrows, dots, fallback messages
defaultColor card color for events that don't set their own
defaultView "carousel" or "grid"
═══════════════════════════════════════════════════════════════ */
const eventsData = {
sections: [
{
id: "national",
title: "National Retreats",
blurb: "Our flagship gatherings, open to young adults across the country.",
accent: "#138ba0",
defaultColor: "#138ba0",
defaultView: "carousel",
background: "#eef9fb",
events: [
{
id: "spring-2025",
title: "Spring Retreat 2026",
theme: "Altering Intertia",
date: "March/April 2026",
location: "Unity Village, MO",
org_logo: "ngu-logo-white-bg.svg",
image: "fall-retreat-logo.svg",
instagram: "@nextgenerationunity",
color: "#f1c2fe",
gradient: "linear-gradient(150deg, rgba(240, 224, 254, 1), rgba(255, 255, 255, 0.28))",
desc_a: "A weekend of connection, workshops, and community for young adults across the Unity movement.",
desc_b: null,
status: "past",
links: []
},
{
id: "fall-retreat-2026",
title: "Fall Retreat 2026",
theme: "Consciousness Creates",
date: "November 12-15th, 2026",
location: "Unity Village, MO",
org_logo: "ngu-logo-white-bg.svg",
image: "fall-retreat-logo.svg",
instagram: "@nextgenerationunity",
color: "#b89421",
gradient: "linear-gradient(150deg, rgba(178, 150, 42, 0.45), rgba(230, 200, 120, 0.15) 65%, rgba(255, 255, 255, 0.28))",
desc_a: "Join us for an exciting opportunity to connect with young adults from across the country through meaningful conversations, creative workshops, and shared artistic expression. All designed to shift your focus to your highest self.",
desc_b: "Registration starting at $150, and $75 lodging cost.",
status: "upcoming",
links: [
{
label: "Register Now!",
link: "https://ngu.churchcenter.com/registrations/events/3761999"
},
{
label: "Scholarship Application",
link: "https://ngu.churchcenter.com/people/forms/1261992"
},
{
label: "Volunteer",
link: "https://ngu.churchcenter.com/people/forms/1176908"
}
]
},
{
id: "spring-recharge-2027",
title: "Spring Recharge 2027",
theme: "TBD",
date: "March 6th, 2027",
location: "Online",
org_logo: "ngu-logo-white-bg.svg",
image: null,
instagram: "@nextgenerationunity",
color: "#138ba0",
gradient: null,
desc_a: "One-day online event to reconnect in the spring.",
desc_b: null,
status: "upcoming",
links: []
},
{
id: "spring-service-2027",
title: "Service Week 2027",
theme: "Leadership & Service",
date: "April 4-9th, 2027",
location: "Unity Village, MO",
org_logo: "ngu-logo-white-bg.svg",
image: null,
instagram: "@nextgenerationunity",
color: "#138ba0",
gradient: null,
desc_a: "Join us at beautiful Unity Village for a week of leadership development and service projects.",
desc_b: null,
status: "upcoming",
links: []
}
]
},
{
id: "regional",
title: "Regional Retreats",
blurb: "Smaller gatherings hosted by regions throughout the year.",
accent: "#aac992",
defaultColor: "#aac992",
defaultView: "grid",
background: "#ffffff",
events: [
{
id: "northwest-2026",
title: "Northwest Regional 2026",
theme: "Theme name",
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: "@nw.ngu",
color: null,
gradient: null,
desc_a: "Short description of the regional retreat goes here.",
desc_b: null,
status: "past",
links: []
},
{
id: "northwest-2027",
title: "Northwest Regional 2027",
theme: "Theme name",
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: "@nw.ngu",
color: null,
gradient: null,
desc_a: "Short description of the regional retreat goes here.",
desc_b: null,
status: "upcoming",
links: []
},
{
id: "northwest-2028",
title: "Northwest Regional 2028",
theme: "Theme name",
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: "@nw.ngu",
color: null,
gradient: null,
desc_a: "Short description of the regional retreat goes here.",
desc_b: null,
status: "upcoming",
links: []
}
]
},
{
id: "partner",
title: "Partner Events",
blurb: "Events hosted by organizations we collaborate with.",
accent: "#7a5ea8",
defaultColor: "#7a5ea8",
defaultView: "grid",
background: "#eef9fb",
events: [
{
id: "partner-example-1",
title: "Partner Event Name",
theme: null,
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: null,
color: null,
gradient: null,
desc_a: "Short description of the partner event goes here.",
desc_b: "Hosted by Partner Organization.",
status: "past",
links: [
{
label: "Learn More",
link: "partner-link"
}
]
},
{
id: "partner-example-2",
title: "Partner Event Name",
theme: null,
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: null,
color: null,
gradient: null,
desc_a: "Short description of the partner event goes here.",
desc_b: "Hosted by Partner Organization.",
status: "past",
links: [
{
label: "Learn More",
link: "partner-link"
}
]
},
{
id: "partner-example-3",
title: "Partner Event Name",
theme: null,
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: null,
color: null,
gradient: null,
desc_a: "Short description of the partner event goes here.",
desc_b: "Hosted by Partner Organization.",
status: "upcoming",
links: [
{
label: "Learn More",
link: "partner-link"
}
]
},
{
id: "partner-example-4",
title: "Partner Event Name",
theme: null,
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: null,
color: null,
gradient: null,
desc_a: "Short description of the partner event goes here.",
desc_b: "Hosted by Partner Organization.",
status: "upcoming",
links: [
{
label: "Learn More",
link: "partner-link"
}
]
},
{
id: "partner-example-5",
title: "Partner Event Name",
theme: null,
date: "Date",
location: "Location",
org_logo: null,
image: null,
instagram: null,
color: null,
gradient: null,
desc_a: "Short description of the partner event goes here.",
desc_b: "Hosted by Partner Organization.",
status: "upcoming",
links: [
{
label: "Learn More",
link: "partner-link"
}
]
}
]
}
]
};
export default eventsData;