@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.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Hono } from 'hono';
|
|
|
4
4
|
import { cors } from 'hono/cors';
|
|
5
5
|
import { drizzle } from 'drizzle-orm/d1';
|
|
6
6
|
import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
7
|
-
import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
|
|
7
|
+
import { sql, relations, eq, and, count, isNotNull, lte } from 'drizzle-orm';
|
|
8
8
|
import { createMiddleware } from 'hono/factory';
|
|
9
9
|
import { betterAuth } from 'better-auth';
|
|
10
10
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
@@ -950,7 +950,10 @@ var GFormsService = class {
|
|
|
950
950
|
const response = await fetch(url.toString(), {
|
|
951
951
|
headers: { Authorization: `Bearer ${token}` }
|
|
952
952
|
});
|
|
953
|
-
if (!response.ok)
|
|
953
|
+
if (!response.ok) {
|
|
954
|
+
const err = await response.text();
|
|
955
|
+
throw new Error(`Forms API error: ${response.status} ${err}`);
|
|
956
|
+
}
|
|
954
957
|
const data = await response.json();
|
|
955
958
|
allResponses.push(...data.responses ?? []);
|
|
956
959
|
pageToken = data.nextPageToken;
|
|
@@ -1224,6 +1227,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
1224
1227
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
1225
1228
|
return c.json({ data: info });
|
|
1226
1229
|
});
|
|
1230
|
+
classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
|
|
1231
|
+
const { slug } = c.req.param();
|
|
1232
|
+
const db = createDb(c.env.DB);
|
|
1233
|
+
const cache = new CacheService(c.env.KV);
|
|
1234
|
+
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1235
|
+
const slots = new SlotsService(db, cache);
|
|
1236
|
+
const event = await db.query.events.findFirst({
|
|
1237
|
+
where: eq(events.slug, slug),
|
|
1238
|
+
columns: { gformsId: true }
|
|
1239
|
+
});
|
|
1240
|
+
if (!event) throw notFound("Event");
|
|
1241
|
+
if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
|
|
1242
|
+
try {
|
|
1243
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1244
|
+
await slots.correctCount(slug, googleCount);
|
|
1245
|
+
return c.json({ data: { registeredSlots: googleCount } });
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
const message = err?.message ?? "Failed to fetch from Google Forms API";
|
|
1248
|
+
return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1227
1251
|
classesRoute.post(
|
|
1228
1252
|
"/",
|
|
1229
1253
|
authMiddleware,
|
|
@@ -1531,6 +1555,196 @@ async function verifyGoogSignature(body, signature, secret) {
|
|
|
1531
1555
|
return false;
|
|
1532
1556
|
}
|
|
1533
1557
|
}
|
|
1558
|
+
var LOCK_KEY = "cron:reconcile-slots:lock";
|
|
1559
|
+
var LOCK_TTL = 300;
|
|
1560
|
+
async function reconcileSlots(env) {
|
|
1561
|
+
const db = createDb(env.DB);
|
|
1562
|
+
const cache = new CacheService(env.KV);
|
|
1563
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1564
|
+
const slots = new SlotsService(db, cache);
|
|
1565
|
+
const lock = await cache.get(LOCK_KEY);
|
|
1566
|
+
if (lock) {
|
|
1567
|
+
console.log("[reconcile-slots] Lock held, skipping.");
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
await cache.set(LOCK_KEY, "1", LOCK_TTL);
|
|
1571
|
+
try {
|
|
1572
|
+
const publishedEvents = await db.query.events.findMany({
|
|
1573
|
+
where: isNotNull(events.gformsId),
|
|
1574
|
+
columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
|
|
1575
|
+
});
|
|
1576
|
+
const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
|
|
1577
|
+
let corrected = 0;
|
|
1578
|
+
for (const event of eventsWithForms) {
|
|
1579
|
+
try {
|
|
1580
|
+
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
1581
|
+
const localCount = event.registeredSlots;
|
|
1582
|
+
if (googleCount !== localCount) {
|
|
1583
|
+
console.warn(
|
|
1584
|
+
`[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
|
|
1585
|
+
);
|
|
1586
|
+
await slots.correctCount(event.slug, googleCount);
|
|
1587
|
+
corrected++;
|
|
1588
|
+
}
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
console.log(
|
|
1594
|
+
`[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
|
|
1595
|
+
);
|
|
1596
|
+
} finally {
|
|
1597
|
+
await cache.del(LOCK_KEY);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/routes/internal/reconcile-slots.ts
|
|
1602
|
+
var reconcileSlotsRoute = new Hono();
|
|
1603
|
+
reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
|
|
1604
|
+
await reconcileSlots(c.env);
|
|
1605
|
+
return c.json({ ok: true });
|
|
1606
|
+
});
|
|
1607
|
+
async function batchRelease(env) {
|
|
1608
|
+
const db = createDb(env.DB);
|
|
1609
|
+
const cache = new CacheService(env.KV);
|
|
1610
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1611
|
+
const toPublish = await db.query.events.findMany({
|
|
1612
|
+
where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
|
|
1613
|
+
columns: { id: true, slug: true }
|
|
1614
|
+
});
|
|
1615
|
+
if (toPublish.length === 0) return;
|
|
1616
|
+
const ids = toPublish.map((e) => e.id);
|
|
1617
|
+
await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
|
|
1618
|
+
// Drizzle doesn't have inArray for D1; use raw SQL for batch
|
|
1619
|
+
sql`${events.id} IN (${sql.join(
|
|
1620
|
+
ids.map((id) => sql`${id}`),
|
|
1621
|
+
sql`, `
|
|
1622
|
+
)})`
|
|
1623
|
+
);
|
|
1624
|
+
await cache.del("events:list");
|
|
1625
|
+
await cache.del("events:etag");
|
|
1626
|
+
console.log(
|
|
1627
|
+
`[batch-release] Published ${toPublish.length} events:`,
|
|
1628
|
+
toPublish.map((e) => e.slug).join(", ")
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/routes/internal/batch-release.ts
|
|
1633
|
+
var batchReleaseRoute = new Hono();
|
|
1634
|
+
batchReleaseRoute.post("/", internalMiddleware, async (c) => {
|
|
1635
|
+
await batchRelease(c.env);
|
|
1636
|
+
return c.json({ ok: true });
|
|
1637
|
+
});
|
|
1638
|
+
function parseStartTimestamp(dateTime, startTime) {
|
|
1639
|
+
if (!dateTime) return null;
|
|
1640
|
+
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
1641
|
+
const ms = Date.parse(combined);
|
|
1642
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
1643
|
+
}
|
|
1644
|
+
async function reminderEmails(env) {
|
|
1645
|
+
if (!env.EMAIL_QUEUE) {
|
|
1646
|
+
console.warn(
|
|
1647
|
+
"[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
|
|
1648
|
+
);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
|
|
1652
|
+
const hasResend = !!env.RESEND_API_KEY;
|
|
1653
|
+
if (!hasSes && !hasResend) {
|
|
1654
|
+
console.warn(
|
|
1655
|
+
"[reminder-emails] No email providers configured. Skipping reminders."
|
|
1656
|
+
);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const db = createDb(env.DB);
|
|
1660
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1661
|
+
const candidates24h = await db.query.events.findMany({
|
|
1662
|
+
where: and(
|
|
1663
|
+
eq(events.status, "published"),
|
|
1664
|
+
eq(events.reminder24hSent, false)
|
|
1665
|
+
),
|
|
1666
|
+
columns: {
|
|
1667
|
+
id: true,
|
|
1668
|
+
dateTime: true,
|
|
1669
|
+
startTime: true
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
for (const event of candidates24h) {
|
|
1673
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1674
|
+
if (!startsAt) continue;
|
|
1675
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1676
|
+
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
1677
|
+
await env.EMAIL_QUEUE.send({
|
|
1678
|
+
type: "send_reminder_email",
|
|
1679
|
+
payload: { eventId: event.id, hoursBeforeEvent: 24 }
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const candidates1h = await db.query.events.findMany({
|
|
1684
|
+
where: and(
|
|
1685
|
+
eq(events.status, "published"),
|
|
1686
|
+
eq(events.reminder1hSent, false)
|
|
1687
|
+
),
|
|
1688
|
+
columns: {
|
|
1689
|
+
id: true,
|
|
1690
|
+
dateTime: true,
|
|
1691
|
+
startTime: true
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
for (const event of candidates1h) {
|
|
1695
|
+
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
1696
|
+
if (!startsAt) continue;
|
|
1697
|
+
const hoursUntil = (startsAt - now) / 3600;
|
|
1698
|
+
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
1699
|
+
await env.EMAIL_QUEUE.send({
|
|
1700
|
+
type: "send_reminder_email",
|
|
1701
|
+
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/routes/internal/reminder-emails.ts
|
|
1708
|
+
var reminderEmailsRoute = new Hono();
|
|
1709
|
+
reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
|
|
1710
|
+
await reminderEmails(c.env);
|
|
1711
|
+
return c.json({ ok: true });
|
|
1712
|
+
});
|
|
1713
|
+
var RENEWAL_WINDOW = 86400;
|
|
1714
|
+
async function renewWatches(env) {
|
|
1715
|
+
const db = createDb(env.DB);
|
|
1716
|
+
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1717
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1718
|
+
const threshold = now + RENEWAL_WINDOW;
|
|
1719
|
+
const expiring = await db.query.events.findMany({
|
|
1720
|
+
where: and(
|
|
1721
|
+
eq(events.status, "published"),
|
|
1722
|
+
lte(events.watchExpiresAt, threshold)
|
|
1723
|
+
),
|
|
1724
|
+
columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
|
|
1725
|
+
});
|
|
1726
|
+
const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
|
|
1727
|
+
let renewed = 0;
|
|
1728
|
+
for (const event of watchEvents) {
|
|
1729
|
+
try {
|
|
1730
|
+
const result = await gforms.renewWatch(event.gformsId, event.watchId);
|
|
1731
|
+
const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
|
|
1732
|
+
await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
|
|
1733
|
+
renewed++;
|
|
1734
|
+
console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/routes/internal/renew-watches.ts
|
|
1743
|
+
var renewWatchesRoute = new Hono();
|
|
1744
|
+
renewWatchesRoute.post("/", internalMiddleware, async (c) => {
|
|
1745
|
+
await renewWatches(c.env);
|
|
1746
|
+
return c.json({ ok: true });
|
|
1747
|
+
});
|
|
1534
1748
|
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1535
1749
|
"image/jpeg",
|
|
1536
1750
|
"image/png",
|
|
@@ -1797,6 +2011,10 @@ function createApp(options = {}) {
|
|
|
1797
2011
|
app.route("/api/faqs", faqsRoute);
|
|
1798
2012
|
app.route("/api/uploads", uploadsRoute);
|
|
1799
2013
|
app.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
2014
|
+
app.route("/internal/reconcile-slots", reconcileSlotsRoute);
|
|
2015
|
+
app.route("/internal/batch-release", batchReleaseRoute);
|
|
2016
|
+
app.route("/internal/reminder-emails", reminderEmailsRoute);
|
|
2017
|
+
app.route("/internal/renew-watches", renewWatchesRoute);
|
|
1800
2018
|
app.onError(errorHandler);
|
|
1801
2019
|
app.notFound(
|
|
1802
2020
|
(c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
|
|
@@ -2227,168 +2445,6 @@ async function processJob(job, services) {
|
|
|
2227
2445
|
}
|
|
2228
2446
|
}
|
|
2229
2447
|
}
|
|
2230
|
-
async function batchRelease(env) {
|
|
2231
|
-
const db = createDb(env.DB);
|
|
2232
|
-
const cache = new CacheService(env.KV);
|
|
2233
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2234
|
-
const toPublish = await db.query.events.findMany({
|
|
2235
|
-
where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
|
|
2236
|
-
columns: { id: true, slug: true }
|
|
2237
|
-
});
|
|
2238
|
-
if (toPublish.length === 0) return;
|
|
2239
|
-
const ids = toPublish.map((e) => e.id);
|
|
2240
|
-
await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
|
|
2241
|
-
// Drizzle doesn't have inArray for D1; use raw SQL for batch
|
|
2242
|
-
sql`${events.id} IN (${sql.join(
|
|
2243
|
-
ids.map((id) => sql`${id}`),
|
|
2244
|
-
sql`, `
|
|
2245
|
-
)})`
|
|
2246
|
-
);
|
|
2247
|
-
await cache.del("events:list");
|
|
2248
|
-
await cache.del("events:etag");
|
|
2249
|
-
console.log(
|
|
2250
|
-
`[batch-release] Published ${toPublish.length} events:`,
|
|
2251
|
-
toPublish.map((e) => e.slug).join(", ")
|
|
2252
|
-
);
|
|
2253
|
-
}
|
|
2254
|
-
var LOCK_KEY = "cron:reconcile-slots:lock";
|
|
2255
|
-
var LOCK_TTL = 300;
|
|
2256
|
-
async function reconcileSlots(env) {
|
|
2257
|
-
const db = createDb(env.DB);
|
|
2258
|
-
const cache = new CacheService(env.KV);
|
|
2259
|
-
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
2260
|
-
const slots = new SlotsService(db, cache);
|
|
2261
|
-
const lock = await cache.get(LOCK_KEY);
|
|
2262
|
-
if (lock) {
|
|
2263
|
-
console.log("[reconcile-slots] Lock held, skipping.");
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
await cache.set(LOCK_KEY, "1", LOCK_TTL);
|
|
2267
|
-
try {
|
|
2268
|
-
const publishedEvents = await db.query.events.findMany({
|
|
2269
|
-
where: eq(events.status, "published"),
|
|
2270
|
-
columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
|
|
2271
|
-
});
|
|
2272
|
-
const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
|
|
2273
|
-
let corrected = 0;
|
|
2274
|
-
for (const event of eventsWithForms) {
|
|
2275
|
-
try {
|
|
2276
|
-
const googleCount = await gforms.getExactResponseCount(event.gformsId);
|
|
2277
|
-
const localCount = event.registeredSlots;
|
|
2278
|
-
if (googleCount !== localCount) {
|
|
2279
|
-
console.warn(
|
|
2280
|
-
`[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
|
|
2281
|
-
);
|
|
2282
|
-
await slots.correctCount(event.slug, googleCount);
|
|
2283
|
-
corrected++;
|
|
2284
|
-
}
|
|
2285
|
-
} catch (err) {
|
|
2286
|
-
console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
console.log(
|
|
2290
|
-
`[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
|
|
2291
|
-
);
|
|
2292
|
-
} finally {
|
|
2293
|
-
await cache.del(LOCK_KEY);
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
function parseStartTimestamp(dateTime, startTime) {
|
|
2297
|
-
if (!dateTime) return null;
|
|
2298
|
-
const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
|
|
2299
|
-
const ms = Date.parse(combined);
|
|
2300
|
-
return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
|
|
2301
|
-
}
|
|
2302
|
-
async function reminderEmails(env) {
|
|
2303
|
-
if (!env.EMAIL_QUEUE) {
|
|
2304
|
-
console.warn(
|
|
2305
|
-
"[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
|
|
2306
|
-
);
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
|
|
2310
|
-
const hasResend = !!env.RESEND_API_KEY;
|
|
2311
|
-
if (!hasSes && !hasResend) {
|
|
2312
|
-
console.warn(
|
|
2313
|
-
"[reminder-emails] No email providers configured. Skipping reminders."
|
|
2314
|
-
);
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
const db = createDb(env.DB);
|
|
2318
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2319
|
-
const candidates24h = await db.query.events.findMany({
|
|
2320
|
-
where: and(
|
|
2321
|
-
eq(events.status, "published"),
|
|
2322
|
-
eq(events.reminder24hSent, false)
|
|
2323
|
-
),
|
|
2324
|
-
columns: {
|
|
2325
|
-
id: true,
|
|
2326
|
-
dateTime: true,
|
|
2327
|
-
startTime: true
|
|
2328
|
-
}
|
|
2329
|
-
});
|
|
2330
|
-
for (const event of candidates24h) {
|
|
2331
|
-
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
2332
|
-
if (!startsAt) continue;
|
|
2333
|
-
const hoursUntil = (startsAt - now) / 3600;
|
|
2334
|
-
if (hoursUntil <= 25 && hoursUntil >= 23) {
|
|
2335
|
-
await env.EMAIL_QUEUE.send({
|
|
2336
|
-
type: "send_reminder_email",
|
|
2337
|
-
payload: { eventId: event.id, hoursBeforeEvent: 24 }
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
const candidates1h = await db.query.events.findMany({
|
|
2342
|
-
where: and(
|
|
2343
|
-
eq(events.status, "published"),
|
|
2344
|
-
eq(events.reminder1hSent, false)
|
|
2345
|
-
),
|
|
2346
|
-
columns: {
|
|
2347
|
-
id: true,
|
|
2348
|
-
dateTime: true,
|
|
2349
|
-
startTime: true
|
|
2350
|
-
}
|
|
2351
|
-
});
|
|
2352
|
-
for (const event of candidates1h) {
|
|
2353
|
-
const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
|
|
2354
|
-
if (!startsAt) continue;
|
|
2355
|
-
const hoursUntil = (startsAt - now) / 3600;
|
|
2356
|
-
if (hoursUntil <= 1.5 && hoursUntil >= 0) {
|
|
2357
|
-
await env.EMAIL_QUEUE.send({
|
|
2358
|
-
type: "send_reminder_email",
|
|
2359
|
-
payload: { eventId: event.id, hoursBeforeEvent: 1 }
|
|
2360
|
-
});
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
var RENEWAL_WINDOW = 86400;
|
|
2365
|
-
async function renewWatches(env) {
|
|
2366
|
-
const db = createDb(env.DB);
|
|
2367
|
-
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
2368
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2369
|
-
const threshold = now + RENEWAL_WINDOW;
|
|
2370
|
-
const expiring = await db.query.events.findMany({
|
|
2371
|
-
where: and(
|
|
2372
|
-
eq(events.status, "published"),
|
|
2373
|
-
lte(events.watchExpiresAt, threshold)
|
|
2374
|
-
),
|
|
2375
|
-
columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
|
|
2376
|
-
});
|
|
2377
|
-
const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
|
|
2378
|
-
let renewed = 0;
|
|
2379
|
-
for (const event of watchEvents) {
|
|
2380
|
-
try {
|
|
2381
|
-
const result = await gforms.renewWatch(event.gformsId, event.watchId);
|
|
2382
|
-
const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
|
|
2383
|
-
await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
|
|
2384
|
-
renewed++;
|
|
2385
|
-
console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
|
|
2386
|
-
} catch (err) {
|
|
2387
|
-
console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
|
|
2391
|
-
}
|
|
2392
2448
|
|
|
2393
2449
|
// src/db/migrate.ts
|
|
2394
2450
|
var PATCH_STATEMENTS = [
|
|
@@ -2595,7 +2651,7 @@ function defaultGetRuntimeConfig(env) {
|
|
|
2595
2651
|
};
|
|
2596
2652
|
}
|
|
2597
2653
|
function injectConfig(html, config) {
|
|
2598
|
-
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)}
|
|
2654
|
+
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
|
|
2599
2655
|
return html.replace("</head>", `${configScript}</head>`);
|
|
2600
2656
|
}
|
|
2601
2657
|
function createWorkerHandler(options) {
|
|
@@ -2717,10 +2773,10 @@ function getRuntimeConfig(env) {
|
|
|
2717
2773
|
};
|
|
2718
2774
|
}
|
|
2719
2775
|
function injectConfig2(html, config) {
|
|
2720
|
-
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)}
|
|
2776
|
+
const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
|
|
2721
2777
|
return html.replace("</head>", `${configScript}</head>`);
|
|
2722
2778
|
}
|
|
2723
2779
|
|
|
2724
2780
|
export { authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, siteConfig, themes, themesRelations, users };
|
|
2725
2781
|
//# sourceMappingURL=index.js.map
|
|
2726
|
-
//# sourceMappingURL=index.js.
|
|
2782
|
+
//# sourceMappingURL=index.js.map
|