Compare commits
No commits in common. "e96d04c317c097dc4e271751a8dcb365e33b5370" and "c185e4423a1ccd58b52d70a41dd51b2c067c9bd7" have entirely different histories.
e96d04c317
...
c185e4423a
9 changed files with 105 additions and 246 deletions
|
|
@ -255,41 +255,6 @@ The Forgejo Actions workflow (`.forgejo/workflows/deploy.yml`) triggers on push
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Countdown Static Site (Caddy)
|
|
||||||
|
|
||||||
`deploy-countdown.yml` provisions a **fresh standalone VPS** that serves only the
|
|
||||||
static `countdown.html` (repo root). It installs Caddy, uploads the page, and lets
|
|
||||||
Caddy obtain + auto-renew the TLS certificate for `hl-mutter-teresa-chemnitz.de`
|
|
||||||
and `www.hl-mutter-teresa-chemnitz.de`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd infra/ansible
|
|
||||||
|
|
||||||
# 1. Point the inventory at the VPS and confirm the domains
|
|
||||||
# edit inventory/countdown/hosts.yml -> ansible_host, countdown_domain, countdown_aliases
|
|
||||||
|
|
||||||
# 2. Make sure DNS A/AAAA records for both names point at the VPS first
|
|
||||||
# (Caddy's ACME challenge needs them resolving), then run with the root
|
|
||||||
# password (--ask-pass needs `sshpass` installed locally):
|
|
||||||
ansible-playbook playbooks/deploy-countdown.yml -i inventory/countdown/hosts.yml --ask-pass
|
|
||||||
```
|
|
||||||
|
|
||||||
> Authenticating with the root password? Install `sshpass` first
|
|
||||||
> (`brew install hudochenkov/sshpass/sshpass` on macOS, `apt install sshpass` on
|
|
||||||
> Debian/Ubuntu). Once you've added an SSH key to the box you can drop
|
|
||||||
> `--ask-pass` and set `ansible_ssh_private_key_file` in the inventory instead.
|
|
||||||
|
|
||||||
**What it does:**
|
|
||||||
|
|
||||||
1. Installs Caddy + ufw (allows 22/80/443)
|
|
||||||
2. Uploads `countdown.html` to `/var/www/countdown/index.html`
|
|
||||||
3. Deploys a Caddyfile serving both domains; Caddy handles HTTPS automatically
|
|
||||||
|
|
||||||
**Re-running** is the normal way to publish content changes — it re-uploads the
|
|
||||||
HTML and reloads Caddy. The certificate is managed entirely by Caddy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Production Setup
|
## Production Setup
|
||||||
|
|
||||||
1. Copy and edit the production inventory:
|
1. Copy and edit the production inventory:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { CollectionConfig } from 'payload'
|
import { CollectionConfig } from 'payload'
|
||||||
import { hide, isAdminOrEmployee } from '@/collections/access/admin'
|
import { hide, isAdminOrEmployee } from '@/collections/access/admin'
|
||||||
import { regenerateMassesForChurch } from '@/jobs/generateRecurringMasses'
|
|
||||||
|
|
||||||
export const Churches: CollectionConfig = {
|
export const Churches: CollectionConfig = {
|
||||||
slug: 'church',
|
slug: 'church',
|
||||||
|
|
@ -215,23 +214,16 @@ export const Churches: CollectionConfig = {
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [
|
afterChange: [
|
||||||
async ({ doc, req }) => {
|
async ({ doc, req }) => {
|
||||||
|
if (!doc.recurringSchedule?.length) return
|
||||||
try {
|
try {
|
||||||
// Materialize Worship docs synchronously so they appear right
|
await req.payload.jobs.queue({
|
||||||
// after saving — same approach as Events/eventOccurrence. This
|
task: 'generateRecurringMasses',
|
||||||
// wipes the church's future auto-generated masses and rebuilds
|
input: { churchId: doc.id },
|
||||||
// them from the current schedule, so changing (or clearing) the
|
|
||||||
// schedule never leaves stale/double masses behind. The weekly
|
|
||||||
// job (generateRecurringMasses) still backfills the rolling
|
|
||||||
// window across all churches via its cron schedule.
|
|
||||||
await regenerateMassesForChurch({
|
|
||||||
church: doc,
|
|
||||||
payload: req.payload,
|
|
||||||
req,
|
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.payload.logger.error(
|
req.payload.logger.error(
|
||||||
{ err, churchId: doc.id },
|
{ err, churchId: doc.id },
|
||||||
'Failed to regenerate recurring masses',
|
'Failed to queue generateRecurringMasses job',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,7 @@ type ItemGroupProps = {
|
||||||
|
|
||||||
const Item = ({href, title, description, icon, onClick}: ItemProps) => {
|
const Item = ({href, title, description, icon, onClick}: ItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link href={href} className={styles.item} onClick={onClick}>
|
||||||
href={href}
|
|
||||||
className={styles.item}
|
|
||||||
onClick={onClick}
|
|
||||||
target={href.includes("/api") ? "_blank" : undefined}
|
|
||||||
>
|
|
||||||
{icon &&
|
{icon &&
|
||||||
<div className={styles.itemIcon}>
|
<div className={styles.itemIcon}>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.groupTitle {
|
.groupTitle {
|
||||||
margin: 10px 0;
|
margin: 0;
|
||||||
color: $dark-text;
|
color: $dark-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
flex: 1 1 40%;
|
flex: 1 1 40%;
|
||||||
height: 80px;
|
height: 100px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: $shade3;
|
background-color: $shade3;
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,6 @@ const MegaMenuItem = ({text, quote, source, groups, onItemClick}: MegaMenuItemPr
|
||||||
<Link
|
<Link
|
||||||
href={""}
|
href={""}
|
||||||
className={styles.menuLink}
|
className={styles.menuLink}
|
||||||
onClick={() => setIsActive(!isActive)}
|
|
||||||
>
|
>
|
||||||
{text} <CollapsibleArrow direction={isActive ? "UP" : "DOWN"} stroke={1.5} />
|
{text} <CollapsibleArrow direction={isActive ? "UP" : "DOWN"} stroke={1.5} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.megaMenuActive {
|
.megaMenuActive {
|
||||||
max-height: 800px;
|
max-height: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
|
@ -131,10 +131,6 @@
|
||||||
position: inherit;
|
position: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.megaMenuActive {
|
|
||||||
max-height: 1500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navMobile {
|
.navMobile {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import type { PayloadRequest, TaskConfig } from 'payload'
|
import type { TaskConfig } from 'payload'
|
||||||
import type { Payload } from 'payload'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
combineDateAndTime,
|
combineDateAndTime,
|
||||||
|
|
@ -19,9 +18,8 @@ import {
|
||||||
* documents and can be edited by hand (cancel, change celebrant, etc.).
|
* documents and can be edited by hand (cancel, change celebrant, etc.).
|
||||||
*
|
*
|
||||||
* Triggers:
|
* Triggers:
|
||||||
* - `afterChange` hook on Church (see collections/Churches.ts) calls
|
* - `afterChange` hook on Church (see collections/Churches.ts) queues
|
||||||
* `regenerateMassesForChurch` directly so generated Worship docs appear
|
* a run whenever a schedule is saved
|
||||||
* immediately on save — same pattern as Events/eventOccurrence.
|
|
||||||
* - Own `schedule` entry below runs weekly to keep the rolling window
|
* - Own `schedule` entry below runs weekly to keep the rolling window
|
||||||
* populated
|
* populated
|
||||||
* - Manual: call `payload.jobs.queue({ task: 'generateRecurringMasses' })`
|
* - Manual: call `payload.jobs.queue({ task: 'generateRecurringMasses' })`
|
||||||
|
|
@ -29,17 +27,9 @@ import {
|
||||||
|
|
||||||
// How far into the future we materialize documents on each run.
|
// How far into the future we materialize documents on each run.
|
||||||
// The weekly cron keeps this rolling window populated.
|
// The weekly cron keeps this rolling window populated.
|
||||||
const DEFAULT_WEEKS_AHEAD = 4
|
const DEFAULT_WEEKS_AHEAD = 2
|
||||||
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000
|
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
// Most recent Monday at local midnight (Monday itself if `d` is a Monday).
|
|
||||||
const startOfLastMonday = (d: Date): Date => {
|
|
||||||
const date = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
|
||||||
const daysSinceMonday = (date.getDay() + 6) % 7 // 0=Sun..6=Sat → Mon=0
|
|
||||||
date.setDate(date.getDate() - daysSinceMonday)
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
|
|
||||||
type GenerateRecurringMassesInput = {
|
type GenerateRecurringMassesInput = {
|
||||||
weeksAhead?: number
|
weeksAhead?: number
|
||||||
churchId?: string
|
churchId?: string
|
||||||
|
|
@ -47,161 +37,9 @@ type GenerateRecurringMassesInput = {
|
||||||
|
|
||||||
type GenerateRecurringMassesOutput = {
|
type GenerateRecurringMassesOutput = {
|
||||||
created: number
|
created: number
|
||||||
deleted: number
|
|
||||||
skipped: number
|
skipped: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChurchLike = {
|
|
||||||
id: string
|
|
||||||
recurringSchedule?: unknown[] | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Materializes a single Church's `recurringSchedule` into real `Worship`
|
|
||||||
* documents for the rolling [last Monday, horizon] window.
|
|
||||||
*
|
|
||||||
* Wipe-and-regenerate: every future auto-generated (`generated: true`)
|
|
||||||
* Worship doc for this church is deleted and rebuilt from the current
|
|
||||||
* schedule, so a changed/cleared schedule never leaves stale or double
|
|
||||||
* masses behind. Hand-created Worship docs are never touched. Manual
|
|
||||||
* cancellations on generated docs are snapshotted by date and re-applied
|
|
||||||
* so they survive the regen (mirrors Events/eventOccurrence).
|
|
||||||
*
|
|
||||||
* Exposed both as a direct function (called from the Church `afterChange`
|
|
||||||
* hook so edits reflect immediately) and via the Jobs Queue task below
|
|
||||||
* (weekly cron backfill across all churches).
|
|
||||||
*/
|
|
||||||
export const regenerateMassesForChurch = async ({
|
|
||||||
church,
|
|
||||||
payload,
|
|
||||||
req,
|
|
||||||
weeksAhead = DEFAULT_WEEKS_AHEAD,
|
|
||||||
now = new Date(),
|
|
||||||
}: {
|
|
||||||
church: ChurchLike
|
|
||||||
payload: Payload
|
|
||||||
req?: PayloadRequest
|
|
||||||
weeksAhead?: number
|
|
||||||
now?: Date
|
|
||||||
}): Promise<GenerateRecurringMassesOutput> => {
|
|
||||||
const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)
|
|
||||||
|
|
||||||
// Start the window at the most recent Monday (local midnight) rather than
|
|
||||||
// `now`, so masses earlier in the current week are still materialized.
|
|
||||||
const windowStart = startOfLastMonday(now)
|
|
||||||
|
|
||||||
let created = 0
|
|
||||||
let skipped = 0
|
|
||||||
|
|
||||||
// Snapshot manual cancellations on generated docs so they survive the
|
|
||||||
// wipe/regen. Matched by ISO timestamp because regen rebuilds the same
|
|
||||||
// (church, date) slots from the schedule.
|
|
||||||
const existing = await payload.find({
|
|
||||||
collection: 'worship',
|
|
||||||
where: {
|
|
||||||
and: [
|
|
||||||
{ location: { equals: church.id } },
|
|
||||||
{ generated: { equals: true } },
|
|
||||||
{ date: { greater_than_equal: windowStart.toISOString() } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
depth: 0,
|
|
||||||
limit: 1000,
|
|
||||||
pagination: false,
|
|
||||||
req,
|
|
||||||
})
|
|
||||||
const cancelledDates = new Set(
|
|
||||||
existing.docs
|
|
||||||
.filter((doc) => doc.cancelled === true && typeof doc.date === 'string')
|
|
||||||
.map((doc) => new Date(doc.date as string).toISOString()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wipe future auto-generated docs, then regenerate from the current
|
|
||||||
// schedule. This is what keeps a changed schedule from leaving stale
|
|
||||||
// (double) masses behind. Hand-created Worship docs (generated !== true)
|
|
||||||
// are never touched.
|
|
||||||
const wipe = await payload.delete({
|
|
||||||
collection: 'worship',
|
|
||||||
where: {
|
|
||||||
and: [
|
|
||||||
{ location: { equals: church.id } },
|
|
||||||
{ generated: { equals: true } },
|
|
||||||
{ date: { greater_than_equal: windowStart.toISOString() } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
req,
|
|
||||||
})
|
|
||||||
const deleted = wipe.docs.length
|
|
||||||
|
|
||||||
const schedule = church.recurringSchedule
|
|
||||||
if (!Array.isArray(schedule) || schedule.length === 0) {
|
|
||||||
return { created, deleted, skipped }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rawEntry of schedule) {
|
|
||||||
const entry = rawEntry as ScheduleEntry & {
|
|
||||||
time?: string | Date | null
|
|
||||||
type?: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER'
|
|
||||||
defaultCelebrant?: string | null
|
|
||||||
defaultTitle?: string | null
|
|
||||||
defaultDescription?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guard: required fields may be missing if an editor saved a
|
|
||||||
// half-filled row. Skip rather than crash the whole run.
|
|
||||||
if (!entry.time || !entry.type) {
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the entry's recurrence pattern (weekly / biweekly /
|
|
||||||
// monthly Nth weekday) into concrete calendar dates in the
|
|
||||||
// [now, horizon] window. Returns dates at midnight only — we
|
|
||||||
// combine with the time-of-day below.
|
|
||||||
const occurrenceDates = generateOccurrenceDates(entry, windowStart, horizon)
|
|
||||||
if (occurrenceDates.length === 0) {
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const timeSource = new Date(entry.time)
|
|
||||||
|
|
||||||
for (const occurrenceDate of occurrenceDates) {
|
|
||||||
// Build the real Worship.date as (calendar date) + (HH:mm from
|
|
||||||
// the schedule) using local-time components. This is the step
|
|
||||||
// that keeps DST transitions from shifting the wall-clock hour.
|
|
||||||
const date = combineDateAndTime(occurrenceDate, timeSource)
|
|
||||||
|
|
||||||
// Skip anything before the window start (last Monday).
|
|
||||||
if (date.getTime() < windowStart.getTime()) {
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// `generated: true` marks this as auto-created so a later run (or the
|
|
||||||
// wipe above) can target it without touching manual rows. Re-apply any
|
|
||||||
// cancellation the editor had set on this slot before the wipe.
|
|
||||||
await payload.create({
|
|
||||||
collection: 'worship',
|
|
||||||
data: {
|
|
||||||
date: date.toISOString(),
|
|
||||||
location: church.id,
|
|
||||||
type: entry.type,
|
|
||||||
cancelled: cancelledDates.has(date.toISOString()),
|
|
||||||
title: entry.defaultTitle || undefined,
|
|
||||||
celebrant: entry.defaultCelebrant || undefined,
|
|
||||||
description: entry.defaultDescription || undefined,
|
|
||||||
generated: true,
|
|
||||||
},
|
|
||||||
req,
|
|
||||||
})
|
|
||||||
|
|
||||||
created += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { created, deleted, skipped }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generateRecurringMassesTask: TaskConfig<{
|
export const generateRecurringMassesTask: TaskConfig<{
|
||||||
input: GenerateRecurringMassesInput
|
input: GenerateRecurringMassesInput
|
||||||
output: GenerateRecurringMassesOutput
|
output: GenerateRecurringMassesOutput
|
||||||
|
|
@ -222,7 +60,6 @@ export const generateRecurringMassesTask: TaskConfig<{
|
||||||
],
|
],
|
||||||
outputSchema: [
|
outputSchema: [
|
||||||
{ name: 'created', type: 'number' },
|
{ name: 'created', type: 'number' },
|
||||||
{ name: 'deleted', type: 'number' },
|
|
||||||
{ name: 'skipped', type: 'number' },
|
{ name: 'skipped', type: 'number' },
|
||||||
],
|
],
|
||||||
// Weekly cron to keep the rolling window populated even if nobody
|
// Weekly cron to keep the rolling window populated even if nobody
|
||||||
|
|
@ -238,9 +75,10 @@ export const generateRecurringMassesTask: TaskConfig<{
|
||||||
const { payload } = req
|
const { payload } = req
|
||||||
const weeksAhead = input?.weeksAhead ?? DEFAULT_WEEKS_AHEAD
|
const weeksAhead = input?.weeksAhead ?? DEFAULT_WEEKS_AHEAD
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)
|
||||||
|
|
||||||
// Scope to a single church when invoked with a churchId, otherwise
|
// Scope to a single church when invoked from the Church afterChange
|
||||||
// process every church in one run (weekly cron backfill).
|
// hook, otherwise process every church in one run.
|
||||||
const churchesResult = await payload.find({
|
const churchesResult = await payload.find({
|
||||||
collection: 'church',
|
collection: 'church',
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
|
@ -252,33 +90,108 @@ export const generateRecurringMassesTask: TaskConfig<{
|
||||||
})
|
})
|
||||||
|
|
||||||
let created = 0
|
let created = 0
|
||||||
let deleted = 0
|
|
||||||
let skipped = 0
|
let skipped = 0
|
||||||
|
|
||||||
for (const church of churchesResult.docs) {
|
for (const church of churchesResult.docs) {
|
||||||
// Cast needed because payload-types only exposes recurringSchedule
|
// Cast needed because payload-types only exposes recurringSchedule
|
||||||
// on the full Church interface and payload.find returns a looser
|
// on the full Church interface and payload.find returns a looser
|
||||||
// shape at depth: 0.
|
// shape at depth: 0.
|
||||||
const result = await regenerateMassesForChurch({
|
const schedule = (church as { recurringSchedule?: unknown[] })
|
||||||
church: church as ChurchLike,
|
.recurringSchedule
|
||||||
payload,
|
if (!Array.isArray(schedule) || schedule.length === 0) continue
|
||||||
req,
|
|
||||||
weeksAhead,
|
for (const rawEntry of schedule) {
|
||||||
now,
|
const entry = rawEntry as ScheduleEntry & {
|
||||||
|
time?: string | Date | null
|
||||||
|
type?: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER'
|
||||||
|
defaultCelebrant?: string | null
|
||||||
|
defaultTitle?: string | null
|
||||||
|
defaultDescription?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard: required fields may be missing if an editor saved a
|
||||||
|
// half-filled row. Skip rather than crash the whole run.
|
||||||
|
if (!entry.time || !entry.type) {
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the entry's recurrence pattern (weekly / biweekly /
|
||||||
|
// monthly Nth weekday) into concrete calendar dates in the
|
||||||
|
// [now, horizon] window. Returns dates at midnight only — we
|
||||||
|
// combine with the time-of-day below.
|
||||||
|
const occurrenceDates = generateOccurrenceDates(entry, now, horizon)
|
||||||
|
if (occurrenceDates.length === 0) {
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const timeSource = new Date(entry.time)
|
||||||
|
|
||||||
|
for (const occurrenceDate of occurrenceDates) {
|
||||||
|
// Build the real Worship.date as (calendar date) + (HH:mm from
|
||||||
|
// the schedule) using local-time components. This is the step
|
||||||
|
// that keeps DST transitions from shifting the wall-clock hour.
|
||||||
|
const date = combineDateAndTime(occurrenceDate, timeSource)
|
||||||
|
|
||||||
|
// `now` could land mid-week: skip any occurrence that already
|
||||||
|
// passed earlier today.
|
||||||
|
if (date.getTime() < now.getTime()) {
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append-only: if *any* Worship doc already exists at this
|
||||||
|
// exact (church, timestamp) slot — whether generated by a
|
||||||
|
// previous run or entered manually — leave it alone. This is
|
||||||
|
// what protects admin edits (cancellations, celebrant changes,
|
||||||
|
// etc.) from being overwritten.
|
||||||
|
const existing = await payload.find({
|
||||||
|
collection: 'worship',
|
||||||
|
depth: 0,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ location: { equals: church.id } },
|
||||||
|
{ date: { equals: date.toISOString() } },
|
||||||
|
],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
created += result.created
|
|
||||||
deleted += result.deleted
|
if (existing.docs.length > 0) {
|
||||||
skipped += result.skipped
|
skipped += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// `generated: true` marks this as auto-created so future
|
||||||
|
// cleanup tooling can target it without touching manual rows.
|
||||||
|
await payload.create({
|
||||||
|
collection: 'worship',
|
||||||
|
data: {
|
||||||
|
date: date.toISOString(),
|
||||||
|
location: church.id,
|
||||||
|
type: entry.type,
|
||||||
|
cancelled: false,
|
||||||
|
title: entry.defaultTitle || undefined,
|
||||||
|
celebrant: entry.defaultCelebrant || undefined,
|
||||||
|
description: entry.defaultDescription || undefined,
|
||||||
|
generated: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
created += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts surface on the Payload Jobs admin page as the task output.
|
// Counts surface on the Payload Jobs admin page as the task output.
|
||||||
payload.logger.info(
|
payload.logger.info(
|
||||||
{ created, deleted, skipped, weeksAhead },
|
{ created, skipped, weeksAhead },
|
||||||
'generateRecurringMasses finished',
|
'generateRecurringMasses finished',
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
output: { created, deleted, skipped },
|
output: { created, skipped },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export const Parish = (
|
||||||
link={<CalendarAnnouncementButtons calendar={calendar} announcements={announcement}/>}
|
link={<CalendarAnnouncementButtons calendar={calendar} announcements={announcement}/>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section padding={"small"} paddingBottom={"large"}>
|
<Section>
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
|
|
|
||||||
|
|
@ -2940,7 +2940,6 @@ export interface TaskGenerateRecurringMasses {
|
||||||
};
|
};
|
||||||
output: {
|
output: {
|
||||||
created?: number | null;
|
created?: number | null;
|
||||||
deleted?: number | null;
|
|
||||||
skipped?: number | null;
|
skipped?: number | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue