@access-dlsu/leapify 0.260601.1 → 0.260602.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/auth/auth.d.ts +2 -2
  3. package/dist/{chunk-2JEY6TSO.js → chunk-WTA2QGY5.js} +4 -2
  4. package/dist/chunk-WTA2QGY5.js.map +1 -0
  5. package/dist/{chunk-X4OB4DZ3.cjs → chunk-ZV4TIJXI.cjs} +4 -2
  6. package/dist/chunk-ZV4TIJXI.cjs.map +1 -0
  7. package/dist/client/auth.d.ts +41 -41
  8. package/dist/client/index.cjs +7 -2
  9. package/dist/client/index.cjs.map +1 -1
  10. package/dist/client/index.d.ts +10 -1
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +7 -2
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/types.d.ts +4 -0
  15. package/dist/client/types.d.ts.map +1 -1
  16. package/dist/cron/reconcile-slots.d.ts.map +1 -1
  17. package/dist/db/schema/themes.d.ts +74 -0
  18. package/dist/db/schema/themes.d.ts.map +1 -1
  19. package/dist/index.cjs +251 -179
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +251 -179
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/middleware/turnstile-challenge.cjs +6 -6
  24. package/dist/lib/middleware/turnstile-challenge.d.ts.map +1 -1
  25. package/dist/lib/middleware/turnstile-challenge.js +1 -1
  26. package/dist/routes/internal/batch-release.d.ts +4 -0
  27. package/dist/routes/internal/batch-release.d.ts.map +1 -0
  28. package/dist/routes/internal/reconcile-slots.d.ts +4 -0
  29. package/dist/routes/internal/reconcile-slots.d.ts.map +1 -0
  30. package/dist/routes/internal/reminder-emails.d.ts +4 -0
  31. package/dist/routes/internal/reminder-emails.d.ts.map +1 -0
  32. package/dist/routes/internal/renew-watches.d.ts +4 -0
  33. package/dist/routes/internal/renew-watches.d.ts.map +1 -0
  34. package/dist/routes/themes.d.ts.map +1 -1
  35. package/dist/services/gforms.d.ts.map +1 -1
  36. package/dist/worker.js +249 -175
  37. package/dist/worker.js.map +1 -1
  38. package/package.json +2 -2
  39. package/dist/chunk-2JEY6TSO.js.map +0 -1
  40. package/dist/chunk-X4OB4DZ3.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkX4OB4DZ3_cjs = require('./chunk-X4OB4DZ3.cjs');
3
+ var chunkZV4TIJXI_cjs = require('./chunk-ZV4TIJXI.cjs');
4
4
  var chunkQ7SFCCGT_cjs = require('./chunk-Q7SFCCGT.cjs');
5
5
  var hono = require('hono');
6
6
  var cors = require('hono/cors');
@@ -22,8 +22,6 @@ var LeapifyError = class extends Error {
22
22
  this.code = code;
23
23
  this.name = "LeapifyError";
24
24
  }
25
- statusCode;
26
- code;
27
25
  };
28
26
  var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
29
27
  var domainRestricted = () => new LeapifyError(
@@ -89,6 +87,10 @@ var themes = sqliteCore.sqliteTable("themes", {
89
87
  name: sqliteCore.text("name").notNull().unique(),
90
88
  path: sqliteCore.text("path").notNull().unique(),
91
89
  // e.g. "/pirates-cove"
90
+ imageUrl: sqliteCore.text("image_url"),
91
+ descriptionEn: sqliteCore.text("description_en"),
92
+ descriptionFil: sqliteCore.text("description_fil"),
93
+ sortOrder: sqliteCore.integer("sort_order").notNull().default(0),
92
94
  createdAt: sqliteCore.integer("created_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3)),
93
95
  updatedAt: sqliteCore.integer("updated_at").notNull().$defaultFn(() => Math.floor(Date.now() / 1e3))
94
96
  });
@@ -750,7 +752,6 @@ var CacheService = class {
750
752
  constructor(kv) {
751
753
  this.kv = kv;
752
754
  }
753
- kv;
754
755
  async get(key) {
755
756
  return this.kv.get(key, "json");
756
757
  }
@@ -796,8 +797,6 @@ var SlotsService = class {
796
797
  this.db = db;
797
798
  this.cache = cache;
798
799
  }
799
- db;
800
- cache;
801
800
  kvKey(slug) {
802
801
  return `${SLOT_KV_PREFIX}${slug}`;
803
802
  }
@@ -952,7 +951,10 @@ var GFormsService = class {
952
951
  const response = await fetch(url.toString(), {
953
952
  headers: { Authorization: `Bearer ${token}` }
954
953
  });
955
- if (!response.ok) throw new Error(`Forms API error: ${response.status}`);
954
+ if (!response.ok) {
955
+ const err = await response.text();
956
+ throw new Error(`Forms API error: ${response.status} ${err}`);
957
+ }
956
958
  const data = await response.json();
957
959
  allResponses.push(...data.responses ?? []);
958
960
  pageToken = data.nextPageToken;
@@ -1226,6 +1228,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
1226
1228
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1227
1229
  return c.json({ data: info });
1228
1230
  });
1231
+ classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
1232
+ const { slug } = c.req.param();
1233
+ const db = createDb(c.env.DB);
1234
+ const cache = new CacheService(c.env.KV);
1235
+ const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1236
+ const slots = new SlotsService(db, cache);
1237
+ const event = await db.query.events.findFirst({
1238
+ where: drizzleOrm.eq(events.slug, slug),
1239
+ columns: { gformsId: true }
1240
+ });
1241
+ if (!event) throw notFound("Event");
1242
+ if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
1243
+ try {
1244
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1245
+ await slots.correctCount(slug, googleCount);
1246
+ return c.json({ data: { registeredSlots: googleCount } });
1247
+ } catch (err) {
1248
+ const message = err?.message ?? "Failed to fetch from Google Forms API";
1249
+ return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
1250
+ }
1251
+ });
1229
1252
  classesRoute.post(
1230
1253
  "/",
1231
1254
  authMiddleware,
@@ -1431,7 +1454,7 @@ faqsRoute.get("/", async (c) => {
1431
1454
  const data = await cache.getOrSet(
1432
1455
  FAQS_KV_KEY,
1433
1456
  () => db.query.faqs.findMany({
1434
- orderBy: (t, { asc }) => [asc(t.sortOrder), asc(t.createdAt)]
1457
+ orderBy: (t, { asc: asc2 }) => [asc2(t.sortOrder), asc2(t.createdAt)]
1435
1458
  }),
1436
1459
  FAQS_TTL
1437
1460
  );
@@ -1533,6 +1556,196 @@ async function verifyGoogSignature(body, signature, secret) {
1533
1556
  return false;
1534
1557
  }
1535
1558
  }
1559
+ var LOCK_KEY = "cron:reconcile-slots:lock";
1560
+ var LOCK_TTL = 300;
1561
+ async function reconcileSlots(env) {
1562
+ const db = createDb(env.DB);
1563
+ const cache = new CacheService(env.KV);
1564
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1565
+ const slots = new SlotsService(db, cache);
1566
+ const lock = await cache.get(LOCK_KEY);
1567
+ if (lock) {
1568
+ console.log("[reconcile-slots] Lock held, skipping.");
1569
+ return;
1570
+ }
1571
+ await cache.set(LOCK_KEY, "1", LOCK_TTL);
1572
+ try {
1573
+ const publishedEvents = await db.query.events.findMany({
1574
+ where: drizzleOrm.isNotNull(events.gformsId),
1575
+ columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
1576
+ });
1577
+ const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
1578
+ let corrected = 0;
1579
+ for (const event of eventsWithForms) {
1580
+ try {
1581
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1582
+ const localCount = event.registeredSlots;
1583
+ if (googleCount !== localCount) {
1584
+ console.warn(
1585
+ `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
1586
+ );
1587
+ await slots.correctCount(event.slug, googleCount);
1588
+ corrected++;
1589
+ }
1590
+ } catch (err) {
1591
+ console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
1592
+ }
1593
+ }
1594
+ console.log(
1595
+ `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
1596
+ );
1597
+ } finally {
1598
+ await cache.del(LOCK_KEY);
1599
+ }
1600
+ }
1601
+
1602
+ // src/routes/internal/reconcile-slots.ts
1603
+ var reconcileSlotsRoute = new hono.Hono();
1604
+ reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
1605
+ await reconcileSlots(c.env);
1606
+ return c.json({ ok: true });
1607
+ });
1608
+ async function batchRelease(env) {
1609
+ const db = createDb(env.DB);
1610
+ const cache = new CacheService(env.KV);
1611
+ const now = Math.floor(Date.now() / 1e3);
1612
+ const toPublish = await db.query.events.findMany({
1613
+ where: drizzleOrm.and(drizzleOrm.eq(events.status, "queued"), drizzleOrm.lte(events.releaseAt, now)),
1614
+ columns: { id: true, slug: true }
1615
+ });
1616
+ if (toPublish.length === 0) return;
1617
+ const ids = toPublish.map((e) => e.id);
1618
+ await db.update(events).set({ status: "published", publishedAt: drizzleOrm.sql`(unixepoch())` }).where(
1619
+ // Drizzle doesn't have inArray for D1; use raw SQL for batch
1620
+ drizzleOrm.sql`${events.id} IN (${drizzleOrm.sql.join(
1621
+ ids.map((id) => drizzleOrm.sql`${id}`),
1622
+ drizzleOrm.sql`, `
1623
+ )})`
1624
+ );
1625
+ await cache.del("events:list");
1626
+ await cache.del("events:etag");
1627
+ console.log(
1628
+ `[batch-release] Published ${toPublish.length} events:`,
1629
+ toPublish.map((e) => e.slug).join(", ")
1630
+ );
1631
+ }
1632
+
1633
+ // src/routes/internal/batch-release.ts
1634
+ var batchReleaseRoute = new hono.Hono();
1635
+ batchReleaseRoute.post("/", internalMiddleware, async (c) => {
1636
+ await batchRelease(c.env);
1637
+ return c.json({ ok: true });
1638
+ });
1639
+ function parseStartTimestamp(dateTime, startTime) {
1640
+ if (!dateTime) return null;
1641
+ const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
1642
+ const ms = Date.parse(combined);
1643
+ return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
1644
+ }
1645
+ async function reminderEmails(env) {
1646
+ if (!env.EMAIL_QUEUE) {
1647
+ console.warn(
1648
+ "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
1649
+ );
1650
+ return;
1651
+ }
1652
+ const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
1653
+ const hasResend = !!env.RESEND_API_KEY;
1654
+ if (!hasSes && !hasResend) {
1655
+ console.warn(
1656
+ "[reminder-emails] No email providers configured. Skipping reminders."
1657
+ );
1658
+ return;
1659
+ }
1660
+ const db = createDb(env.DB);
1661
+ const now = Math.floor(Date.now() / 1e3);
1662
+ const candidates24h = await db.query.events.findMany({
1663
+ where: drizzleOrm.and(
1664
+ drizzleOrm.eq(events.status, "published"),
1665
+ drizzleOrm.eq(events.reminder24hSent, false)
1666
+ ),
1667
+ columns: {
1668
+ id: true,
1669
+ dateTime: true,
1670
+ startTime: true
1671
+ }
1672
+ });
1673
+ for (const event of candidates24h) {
1674
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1675
+ if (!startsAt) continue;
1676
+ const hoursUntil = (startsAt - now) / 3600;
1677
+ if (hoursUntil <= 25 && hoursUntil >= 23) {
1678
+ await env.EMAIL_QUEUE.send({
1679
+ type: "send_reminder_email",
1680
+ payload: { eventId: event.id, hoursBeforeEvent: 24 }
1681
+ });
1682
+ }
1683
+ }
1684
+ const candidates1h = await db.query.events.findMany({
1685
+ where: drizzleOrm.and(
1686
+ drizzleOrm.eq(events.status, "published"),
1687
+ drizzleOrm.eq(events.reminder1hSent, false)
1688
+ ),
1689
+ columns: {
1690
+ id: true,
1691
+ dateTime: true,
1692
+ startTime: true
1693
+ }
1694
+ });
1695
+ for (const event of candidates1h) {
1696
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1697
+ if (!startsAt) continue;
1698
+ const hoursUntil = (startsAt - now) / 3600;
1699
+ if (hoursUntil <= 1.5 && hoursUntil >= 0) {
1700
+ await env.EMAIL_QUEUE.send({
1701
+ type: "send_reminder_email",
1702
+ payload: { eventId: event.id, hoursBeforeEvent: 1 }
1703
+ });
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ // src/routes/internal/reminder-emails.ts
1709
+ var reminderEmailsRoute = new hono.Hono();
1710
+ reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
1711
+ await reminderEmails(c.env);
1712
+ return c.json({ ok: true });
1713
+ });
1714
+ var RENEWAL_WINDOW = 86400;
1715
+ async function renewWatches(env) {
1716
+ const db = createDb(env.DB);
1717
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1718
+ const now = Math.floor(Date.now() / 1e3);
1719
+ const threshold = now + RENEWAL_WINDOW;
1720
+ const expiring = await db.query.events.findMany({
1721
+ where: drizzleOrm.and(
1722
+ drizzleOrm.eq(events.status, "published"),
1723
+ drizzleOrm.lte(events.watchExpiresAt, threshold)
1724
+ ),
1725
+ columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
1726
+ });
1727
+ const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
1728
+ let renewed = 0;
1729
+ for (const event of watchEvents) {
1730
+ try {
1731
+ const result = await gforms.renewWatch(event.gformsId, event.watchId);
1732
+ const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
1733
+ await db.update(events).set({ watchExpiresAt: newExpiry }).where(drizzleOrm.eq(events.id, event.id));
1734
+ renewed++;
1735
+ console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
1736
+ } catch (err) {
1737
+ console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
1738
+ }
1739
+ }
1740
+ console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
1741
+ }
1742
+
1743
+ // src/routes/internal/renew-watches.ts
1744
+ var renewWatchesRoute = new hono.Hono();
1745
+ renewWatchesRoute.post("/", internalMiddleware, async (c) => {
1746
+ await renewWatches(c.env);
1747
+ return c.json({ ok: true });
1748
+ });
1536
1749
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1537
1750
  "image/jpeg",
1538
1751
  "image/png",
@@ -1632,14 +1845,27 @@ function extensionFromMime(mime) {
1632
1845
  };
1633
1846
  return map[mime] ?? "bin";
1634
1847
  }
1848
+ function generatePath(name) {
1849
+ return name.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1850
+ }
1635
1851
  var createThemeSchema = zod.z.object({
1636
1852
  name: zod.z.string().min(1),
1637
- path: zod.z.string().min(1)
1853
+ imageUrl: zod.z.string().url().nullable().optional(),
1854
+ descriptionEn: zod.z.string().nullable().optional(),
1855
+ descriptionFil: zod.z.string().nullable().optional(),
1856
+ sortOrder: zod.z.number().int().default(0)
1857
+ });
1858
+ zod.z.object({
1859
+ name: zod.z.string().min(1).optional(),
1860
+ imageUrl: zod.z.string().url().nullable().optional(),
1861
+ descriptionEn: zod.z.string().nullable().optional(),
1862
+ descriptionFil: zod.z.string().nullable().optional(),
1863
+ sortOrder: zod.z.number().int().optional()
1638
1864
  });
1639
1865
  var themesRoute = new hono.Hono();
1640
1866
  themesRoute.get("/", async (c) => {
1641
1867
  const db = createDb(c.env.DB);
1642
- const data = await db.select().from(themes);
1868
+ const data = await db.select().from(themes).orderBy(drizzleOrm.asc(themes.sortOrder), drizzleOrm.asc(themes.createdAt));
1643
1869
  return c.json({ data });
1644
1870
  });
1645
1871
  themesRoute.post(
@@ -1650,8 +1876,9 @@ themesRoute.post(
1650
1876
  async (c) => {
1651
1877
  const body = c.req.valid("json");
1652
1878
  const db = createDb(c.env.DB);
1879
+ const path = generatePath(body.name);
1653
1880
  try {
1654
- const [created] = await db.insert(themes).values(body).returning();
1881
+ const [created] = await db.insert(themes).values({ ...body, path }).returning();
1655
1882
  return c.json({ data: created }, 201);
1656
1883
  } catch (err) {
1657
1884
  if (err.message && err.message.includes("UNIQUE constraint failed")) {
@@ -1669,8 +1896,12 @@ themesRoute.patch(
1669
1896
  const { id } = c.req.param();
1670
1897
  const body = await c.req.json();
1671
1898
  const db = createDb(c.env.DB);
1899
+ const update = { ...body };
1900
+ if (body.name) {
1901
+ update.path = generatePath(body.name);
1902
+ }
1672
1903
  try {
1673
- const [updated] = await db.update(themes).set(body).where(drizzleOrm.eq(themes.id, id)).returning();
1904
+ const [updated] = await db.update(themes).set(update).where(drizzleOrm.eq(themes.id, id)).returning();
1674
1905
  if (!updated) throw notFound("Theme");
1675
1906
  return c.json({ data: updated });
1676
1907
  } catch (err) {
@@ -1758,7 +1989,7 @@ function createApp(options = {}) {
1758
1989
  });
1759
1990
  }
1760
1991
  app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
1761
- app.use("*", chunkX4OB4DZ3_cjs.createTurnstileMiddleware());
1992
+ app.use("*", chunkZV4TIJXI_cjs.createTurnstileMiddleware());
1762
1993
  app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
1763
1994
  app.on(["POST", "GET"], "/api/auth/*", (c) => {
1764
1995
  const auth = createAuth(c.env);
@@ -1789,7 +2020,7 @@ function createApp(options = {}) {
1789
2020
  }
1790
2021
  return next();
1791
2022
  });
1792
- app.post(chunkX4OB4DZ3_cjs.TURNSTILE_VERIFY_PATH, chunkX4OB4DZ3_cjs.handleTurnstileVerify);
2023
+ app.post(chunkZV4TIJXI_cjs.TURNSTILE_VERIFY_PATH, chunkZV4TIJXI_cjs.handleTurnstileVerify);
1793
2024
  app.route("/health", healthRoute);
1794
2025
  app.route("/api/config", siteConfigRoute);
1795
2026
  app.route("/api/classes", classesRoute);
@@ -1799,6 +2030,10 @@ function createApp(options = {}) {
1799
2030
  app.route("/api/faqs", faqsRoute);
1800
2031
  app.route("/api/uploads", uploadsRoute);
1801
2032
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
2033
+ app.route("/internal/reconcile-slots", reconcileSlotsRoute);
2034
+ app.route("/internal/batch-release", batchReleaseRoute);
2035
+ app.route("/internal/reminder-emails", reminderEmailsRoute);
2036
+ app.route("/internal/renew-watches", renewWatchesRoute);
1802
2037
  app.onError(errorHandler);
1803
2038
  app.notFound(
1804
2039
  (c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
@@ -1904,7 +2139,6 @@ var SesError = class extends Error {
1904
2139
  this.status = status;
1905
2140
  this.name = "SesError";
1906
2141
  }
1907
- status;
1908
2142
  /**
1909
2143
  * True for errors that are permanent (not worth retrying via SES again).
1910
2144
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.
@@ -2229,168 +2463,6 @@ async function processJob(job, services) {
2229
2463
  }
2230
2464
  }
2231
2465
  }
2232
- async function batchRelease(env) {
2233
- const db = createDb(env.DB);
2234
- const cache = new CacheService(env.KV);
2235
- const now = Math.floor(Date.now() / 1e3);
2236
- const toPublish = await db.query.events.findMany({
2237
- where: drizzleOrm.and(drizzleOrm.eq(events.status, "queued"), drizzleOrm.lte(events.releaseAt, now)),
2238
- columns: { id: true, slug: true }
2239
- });
2240
- if (toPublish.length === 0) return;
2241
- const ids = toPublish.map((e) => e.id);
2242
- await db.update(events).set({ status: "published", publishedAt: drizzleOrm.sql`(unixepoch())` }).where(
2243
- // Drizzle doesn't have inArray for D1; use raw SQL for batch
2244
- drizzleOrm.sql`${events.id} IN (${drizzleOrm.sql.join(
2245
- ids.map((id) => drizzleOrm.sql`${id}`),
2246
- drizzleOrm.sql`, `
2247
- )})`
2248
- );
2249
- await cache.del("events:list");
2250
- await cache.del("events:etag");
2251
- console.log(
2252
- `[batch-release] Published ${toPublish.length} events:`,
2253
- toPublish.map((e) => e.slug).join(", ")
2254
- );
2255
- }
2256
- var LOCK_KEY = "cron:reconcile-slots:lock";
2257
- var LOCK_TTL = 300;
2258
- async function reconcileSlots(env) {
2259
- const db = createDb(env.DB);
2260
- const cache = new CacheService(env.KV);
2261
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2262
- const slots = new SlotsService(db, cache);
2263
- const lock = await cache.get(LOCK_KEY);
2264
- if (lock) {
2265
- console.log("[reconcile-slots] Lock held, skipping.");
2266
- return;
2267
- }
2268
- await cache.set(LOCK_KEY, "1", LOCK_TTL);
2269
- try {
2270
- const publishedEvents = await db.query.events.findMany({
2271
- where: drizzleOrm.eq(events.status, "published"),
2272
- columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
2273
- });
2274
- const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
2275
- let corrected = 0;
2276
- for (const event of eventsWithForms) {
2277
- try {
2278
- const googleCount = await gforms.getExactResponseCount(event.gformsId);
2279
- const localCount = event.registeredSlots;
2280
- if (googleCount !== localCount) {
2281
- console.warn(
2282
- `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
2283
- );
2284
- await slots.correctCount(event.slug, googleCount);
2285
- corrected++;
2286
- }
2287
- } catch (err) {
2288
- console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
2289
- }
2290
- }
2291
- console.log(
2292
- `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
2293
- );
2294
- } finally {
2295
- await cache.del(LOCK_KEY);
2296
- }
2297
- }
2298
- function parseStartTimestamp(dateTime, startTime) {
2299
- if (!dateTime) return null;
2300
- const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
2301
- const ms = Date.parse(combined);
2302
- return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
2303
- }
2304
- async function reminderEmails(env) {
2305
- if (!env.EMAIL_QUEUE) {
2306
- console.warn(
2307
- "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
2308
- );
2309
- return;
2310
- }
2311
- const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
2312
- const hasResend = !!env.RESEND_API_KEY;
2313
- if (!hasSes && !hasResend) {
2314
- console.warn(
2315
- "[reminder-emails] No email providers configured. Skipping reminders."
2316
- );
2317
- return;
2318
- }
2319
- const db = createDb(env.DB);
2320
- const now = Math.floor(Date.now() / 1e3);
2321
- const candidates24h = await db.query.events.findMany({
2322
- where: drizzleOrm.and(
2323
- drizzleOrm.eq(events.status, "published"),
2324
- drizzleOrm.eq(events.reminder24hSent, false)
2325
- ),
2326
- columns: {
2327
- id: true,
2328
- dateTime: true,
2329
- startTime: true
2330
- }
2331
- });
2332
- for (const event of candidates24h) {
2333
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2334
- if (!startsAt) continue;
2335
- const hoursUntil = (startsAt - now) / 3600;
2336
- if (hoursUntil <= 25 && hoursUntil >= 23) {
2337
- await env.EMAIL_QUEUE.send({
2338
- type: "send_reminder_email",
2339
- payload: { eventId: event.id, hoursBeforeEvent: 24 }
2340
- });
2341
- }
2342
- }
2343
- const candidates1h = await db.query.events.findMany({
2344
- where: drizzleOrm.and(
2345
- drizzleOrm.eq(events.status, "published"),
2346
- drizzleOrm.eq(events.reminder1hSent, false)
2347
- ),
2348
- columns: {
2349
- id: true,
2350
- dateTime: true,
2351
- startTime: true
2352
- }
2353
- });
2354
- for (const event of candidates1h) {
2355
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2356
- if (!startsAt) continue;
2357
- const hoursUntil = (startsAt - now) / 3600;
2358
- if (hoursUntil <= 1.5 && hoursUntil >= 0) {
2359
- await env.EMAIL_QUEUE.send({
2360
- type: "send_reminder_email",
2361
- payload: { eventId: event.id, hoursBeforeEvent: 1 }
2362
- });
2363
- }
2364
- }
2365
- }
2366
- var RENEWAL_WINDOW = 86400;
2367
- async function renewWatches(env) {
2368
- const db = createDb(env.DB);
2369
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2370
- const now = Math.floor(Date.now() / 1e3);
2371
- const threshold = now + RENEWAL_WINDOW;
2372
- const expiring = await db.query.events.findMany({
2373
- where: drizzleOrm.and(
2374
- drizzleOrm.eq(events.status, "published"),
2375
- drizzleOrm.lte(events.watchExpiresAt, threshold)
2376
- ),
2377
- columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
2378
- });
2379
- const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
2380
- let renewed = 0;
2381
- for (const event of watchEvents) {
2382
- try {
2383
- const result = await gforms.renewWatch(event.gformsId, event.watchId);
2384
- const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
2385
- await db.update(events).set({ watchExpiresAt: newExpiry }).where(drizzleOrm.eq(events.id, event.id));
2386
- renewed++;
2387
- console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
2388
- } catch (err) {
2389
- console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
2390
- }
2391
- }
2392
- console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
2393
- }
2394
2466
 
2395
2467
  // src/db/migrate.ts
2396
2468
  var PATCH_STATEMENTS = [
@@ -2597,7 +2669,7 @@ function defaultGetRuntimeConfig(env) {
2597
2669
  };
2598
2670
  }
2599
2671
  function injectConfig(html, config) {
2600
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2672
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2601
2673
  return html.replace("</head>", `${configScript}</head>`);
2602
2674
  }
2603
2675
  function createWorkerHandler(options) {
@@ -2719,7 +2791,7 @@ function getRuntimeConfig(env) {
2719
2791
  };
2720
2792
  }
2721
2793
  function injectConfig2(html, config) {
2722
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2794
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2723
2795
  return html.replace("</head>", `${configScript}</head>`);
2724
2796
  }
2725
2797