@access-dlsu/leapify 0.260601.1 → 0.260601.2
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.
- package/dist/app.d.ts.map +1 -1
- package/dist/client/index.cjs +7 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +7 -0
- package/dist/client/index.js.map +1 -1
- package/dist/cron/reconcile-slots.d.ts.map +1 -1
- package/dist/index.cjs +221 -165
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +223 -167
- package/dist/index.js.map +1 -1
- package/dist/routes/internal/batch-release.d.ts +4 -0
- package/dist/routes/internal/batch-release.d.ts.map +1 -0
- package/dist/routes/internal/reconcile-slots.d.ts +4 -0
- package/dist/routes/internal/reconcile-slots.d.ts.map +1 -0
- package/dist/routes/internal/reminder-emails.d.ts +4 -0
- package/dist/routes/internal/reminder-emails.d.ts.map +1 -0
- package/dist/routes/internal/renew-watches.d.ts +4 -0
- package/dist/routes/internal/renew-watches.d.ts.map +1 -0
- package/dist/services/gforms.d.ts.map +1 -1
- package/dist/worker.js +220 -164
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -952,7 +952,10 @@ var GFormsService = class {
|
|
|
952
952
|
const response = await fetch(url.toString(), {
|
|
953
953
|
headers: { Authorization: `Bearer ${token}` }
|
|
954
954
|
});
|
|
955
|
-
if (!response.ok)
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
const err = await response.text();
|
|
957
|
+
throw new Error(`Forms API error: ${response.status} ${err}`);
|
|
958
|
+
}
|
|
956
959
|
const data = await response.json();
|
|
957
960
|
allResponses.push(...data.responses ?? []);
|
|
958
961
|
pageToken = data.nextPageToken;
|
|
@@ -1226,6 +1229,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
1226
1229
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
1227
1230
|
return c.json({ data: info });
|
|
1228
1231
|
});
|
|
1232
|
+
classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
|
|
1233
|
+
const { slug } = c.req.param();
|
|
1234
|
+
const db = createDb(c.env.DB);
|
|
1235
|
+
const cache = new CacheService(c.env.KV);
|
|
1236
|
+
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1237
|
+
const slots = new SlotsService(db, cache);
|
|
1238
|
+
const event = await db.query.events.findFirst({
|
|
1239
|
+
where: drizzleOrm.eq(events.slug, slug),
|
|
1240
|
+
columns: { gformsId: true }
|
|
1241
|
+
});
|
|
1242
|
+
if (!event) throw notFound("Event");
|
|
1243
|
+
if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
|
|
1244
|
+
try {
|
|
1245
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1246
|
+
await slots.correctCount(slug, googleCount);
|
|
1247
|
+
return c.json({ data: { registeredSlots: googleCount } });
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
const message = err?.message ?? "Failed to fetch from Google Forms API";
|
|
1250
|
+
return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1229
1253
|
classesRoute.post(
|
|
1230
1254
|
"/",
|
|
1231
1255
|
authMiddleware,
|
|
@@ -1533,6 +1557,196 @@ async function verifyGoogSignature(body, signature, secret) {
|
|
|
1533
1557
|
return false;
|
|
1534
1558
|
}
|
|
1535
1559
|
}
|
|
1560
|
+
var LOCK_KEY = "cron:reconcile-slots:lock";
|
|
1561
|
+
var LOCK_TTL = 300;
|
|
1562
|
+
async function reconcileSlots(env) {
|
|
1563
|
+
const db = createDb(env.DB);
|
|
1564
|
+
const cache = new CacheService(env.KV);
|
|
1565
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1566
|
+
const slots = new SlotsService(db, cache);
|
|
1567
|
+
const lock = await cache.get(LOCK_KEY);
|
|
1568
|
+
if (lock) {
|
|
1569
|
+
console.log("[reconcile-slots] Lock held, skipping.");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
await cache.set(LOCK_KEY, "1", LOCK_TTL);
|
|
1573
|
+
try {
|
|
1574
|
+
const publishedEvents = await db.query.events.findMany({
|
|
1575
|
+
where: drizzleOrm.isNotNull(events.gformsId),
|
|
1576
|
+
columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
|
|
1577
|
+
});
|
|
1578
|
+
const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
|
|
1579
|
+
let corrected = 0;
|
|
1580
|
+
for (const event of eventsWithForms) {
|
|
1581
|
+
try {
|
|
1582
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1583
|
+
const localCount = event.registeredSlots;
|
|
1584
|
+
if (googleCount !== localCount) {
|
|
1585
|
+
console.warn(
|
|
1586
|
+
`[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
|
|
1587
|
+
);
|
|
1588
|
+
await slots.correctCount(event.slug, googleCount);
|
|
1589
|
+
corrected++;
|
|
1590
|
+
}
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
console.log(
|
|
1596
|
+
`[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
|
|
1597
|
+
);
|
|
1598
|
+
} finally {
|
|
1599
|
+
await cache.del(LOCK_KEY);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/routes/internal/reconcile-slots.ts
|
|
1604
|
+
var reconcileSlotsRoute = new hono.Hono();
|
|
1605
|
+
reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
|
|
1606
|
+
await reconcileSlots(c.env);
|
|
1607
|
+
return c.json({ ok: true });
|
|
1608
|
+
});
|
|
1609
|
+
async function batchRelease(env) {
|
|
1610
|
+
const db = createDb(env.DB);
|
|
1611
|
+
const cache = new CacheService(env.KV);
|
|
1612
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1613
|
+
const toPublish = await db.query.events.findMany({
|
|
1614
|
+
where: drizzleOrm.and(drizzleOrm.eq(events.status, "queued"), drizzleOrm.lte(events.releaseAt, now)),
|
|
1615
|
+
columns: { id: true, slug: true }
|
|
1616
|
+
});
|
|
1617
|
+
if (toPublish.length === 0) return;
|
|
1618
|
+
const ids = toPublish.map((e) => e.id);
|
|
1619
|
+
await db.update(events).set({ status: "published", publishedAt: drizzleOrm.sql`(unixepoch())` }).where(
|
|
1620
|
+
// Drizzle doesn't have inArray for D1; use raw SQL for batch
|
|
1621
|
+
drizzleOrm.sql`${events.id} IN (${drizzleOrm.sql.join(
|
|
1622
|
+
ids.map((id) => drizzleOrm.sql`${id}`),
|
|
1623
|
+
drizzleOrm.sql`, `
|
|
1624
|
+
)})`
|
|
1625
|
+
);
|
|
1626
|
+
await cache.del("events:list");
|
|
1627
|
+
await cache.del("events:etag");
|
|
1628
|
+
console.log(
|
|
1629
|
+
`[batch-release] Published ${toPublish.length} events:`,
|
|
1630
|
+
toPublish.map((e) => e.slug).join(", ")
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/routes/internal/batch-release.ts
|
|
1635
|
+
var batchReleaseRoute = new hono.Hono();
|
|
1636
|
+
batchReleaseRoute.post("/", internalMiddleware, async (c) => {
|
|
1637
|
+
await batchRelease(c.env);
|
|
1638
|
+
return c.json({ ok: true });
|
|
1639
|
+
});
|
|
1640
|
+
function parseStartTimestamp(dateTime, startTime) {
|
|
1641
|
+
if (!dateTime) return null;
|
|
1642
|
+
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
1643
|
+
const ms = Date.parse(combined);
|
|
1644
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
1645
|
+
}
|
|
1646
|
+
async function reminderEmails(env) {
|
|
1647
|
+
if (!env.EMAIL_QUEUE) {
|
|
1648
|
+
console.warn(
|
|
1649
|
+
"[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
|
|
1650
|
+
);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
|
|
1654
|
+
const hasResend = !!env.RESEND_API_KEY;
|
|
1655
|
+
if (!hasSes && !hasResend) {
|
|
1656
|
+
console.warn(
|
|
1657
|
+
"[reminder-emails] No email providers configured. Skipping reminders."
|
|
1658
|
+
);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
const db = createDb(env.DB);
|
|
1662
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1663
|
+
const candidates24h = await db.query.events.findMany({
|
|
1664
|
+
where: drizzleOrm.and(
|
|
1665
|
+
drizzleOrm.eq(events.status, "published"),
|
|
1666
|
+
drizzleOrm.eq(events.reminder24hSent, false)
|
|
1667
|
+
),
|
|
1668
|
+
columns: {
|
|
1669
|
+
id: true,
|
|
1670
|
+
dateTime: true,
|
|
1671
|
+
startTime: true
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
for (const event of candidates24h) {
|
|
1675
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1676
|
+
if (!startsAt) continue;
|
|
1677
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1678
|
+
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
1679
|
+
await env.EMAIL_QUEUE.send({
|
|
1680
|
+
type: "send_reminder_email",
|
|
1681
|
+
payload: { eventId: event.id, hoursBeforeEvent: 24 }
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const candidates1h = await db.query.events.findMany({
|
|
1686
|
+
where: drizzleOrm.and(
|
|
1687
|
+
drizzleOrm.eq(events.status, "published"),
|
|
1688
|
+
drizzleOrm.eq(events.reminder1hSent, false)
|
|
1689
|
+
),
|
|
1690
|
+
columns: {
|
|
1691
|
+
id: true,
|
|
1692
|
+
dateTime: true,
|
|
1693
|
+
startTime: true
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
for (const event of candidates1h) {
|
|
1697
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1698
|
+
if (!startsAt) continue;
|
|
1699
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1700
|
+
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
1701
|
+
await env.EMAIL_QUEUE.send({
|
|
1702
|
+
type: "send_reminder_email",
|
|
1703
|
+
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/routes/internal/reminder-emails.ts
|
|
1710
|
+
var reminderEmailsRoute = new hono.Hono();
|
|
1711
|
+
reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
|
|
1712
|
+
await reminderEmails(c.env);
|
|
1713
|
+
return c.json({ ok: true });
|
|
1714
|
+
});
|
|
1715
|
+
var RENEWAL_WINDOW = 86400;
|
|
1716
|
+
async function renewWatches(env) {
|
|
1717
|
+
const db = createDb(env.DB);
|
|
1718
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1719
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1720
|
+
const threshold = now + RENEWAL_WINDOW;
|
|
1721
|
+
const expiring = await db.query.events.findMany({
|
|
1722
|
+
where: drizzleOrm.and(
|
|
1723
|
+
drizzleOrm.eq(events.status, "published"),
|
|
1724
|
+
drizzleOrm.lte(events.watchExpiresAt, threshold)
|
|
1725
|
+
),
|
|
1726
|
+
columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
|
|
1727
|
+
});
|
|
1728
|
+
const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
|
|
1729
|
+
let renewed = 0;
|
|
1730
|
+
for (const event of watchEvents) {
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await gforms.renewWatch(event.gformsId, event.watchId);
|
|
1733
|
+
const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
|
|
1734
|
+
await db.update(events).set({ watchExpiresAt: newExpiry }).where(drizzleOrm.eq(events.id, event.id));
|
|
1735
|
+
renewed++;
|
|
1736
|
+
console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
|
|
1737
|
+
} catch (err) {
|
|
1738
|
+
console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// src/routes/internal/renew-watches.ts
|
|
1745
|
+
var renewWatchesRoute = new hono.Hono();
|
|
1746
|
+
renewWatchesRoute.post("/", internalMiddleware, async (c) => {
|
|
1747
|
+
await renewWatches(c.env);
|
|
1748
|
+
return c.json({ ok: true });
|
|
1749
|
+
});
|
|
1536
1750
|
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1537
1751
|
"image/jpeg",
|
|
1538
1752
|
"image/png",
|
|
@@ -1799,6 +2013,10 @@ function createApp(options = {}) {
|
|
|
1799
2013
|
app.route("/api/faqs", faqsRoute);
|
|
1800
2014
|
app.route("/api/uploads", uploadsRoute);
|
|
1801
2015
|
app.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
2016
|
+
app.route("/internal/reconcile-slots", reconcileSlotsRoute);
|
|
2017
|
+
app.route("/internal/batch-release", batchReleaseRoute);
|
|
2018
|
+
app.route("/internal/reminder-emails", reminderEmailsRoute);
|
|
2019
|
+
app.route("/internal/renew-watches", renewWatchesRoute);
|
|
1802
2020
|
app.onError(errorHandler);
|
|
1803
2021
|
app.notFound(
|
|
1804
2022
|
(c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
|
|
@@ -2229,168 +2447,6 @@ async function processJob(job, services) {
|
|
|
2229
2447
|
}
|
|
2230
2448
|
}
|
|
2231
2449
|
}
|
|
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
2450
|
|
|
2395
2451
|
// src/db/migrate.ts
|
|
2396
2452
|
var PATCH_STATEMENTS = [
|
|
@@ -2597,7 +2653,7 @@ function defaultGetRuntimeConfig(env) {
|
|
|
2597
2653
|
};
|
|
2598
2654
|
}
|
|
2599
2655
|
function injectConfig(html, config) {
|
|
2600
|
-
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)}
|
|
2656
|
+
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
|
|
2601
2657
|
return html.replace("</head>", `${configScript}</head>`);
|
|
2602
2658
|
}
|
|
2603
2659
|
function createWorkerHandler(options) {
|
|
@@ -2719,7 +2775,7 @@ function getRuntimeConfig(env) {
|
|
|
2719
2775
|
};
|
|
2720
2776
|
}
|
|
2721
2777
|
function injectConfig2(html, config) {
|
|
2722
|
-
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)}
|
|
2778
|
+
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
|
|
2723
2779
|
return html.replace("</head>", `${configScript}</head>`);
|
|
2724
2780
|
}
|
|
2725
2781
|
|