@acms-subir/core 0.2.0 → 0.3.0
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/CHANGELOG.md +78 -0
- package/cli/common.mjs +35 -0
- package/cli/legacy.mjs +317 -0
- package/cli/main.mjs +66 -0
- package/cli/palette.mjs +4 -0
- package/cli/preview.mjs +50 -0
- package/cli/remote.mjs +55 -0
- package/cli/themes.mjs +127 -0
- package/cli.mjs +2 -369
- package/deploy/Caddyfile +8 -0
- package/deploy/Dockerfile +34 -0
- package/deploy/compose.https.yml +21 -0
- package/deploy/compose.yml +46 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +182 -2
- package/ops-check.mjs +27 -0
- package/package.json +27 -5
- package/src/actions/index.ts +575 -47
- package/src/components/AiCleaner.tsx +291 -0
- package/src/components/AiSettings.tsx +575 -0
- package/src/components/CommandPalette.tsx +200 -0
- package/src/components/ContentList.astro +246 -0
- package/src/components/DesignAccess.tsx +182 -0
- package/src/components/DesignManager.tsx +418 -46
- package/src/components/EmailSettings.tsx +521 -0
- package/src/components/EventPage.astro +178 -0
- package/src/components/LegalPage.astro +27 -0
- package/src/components/LinksManager.tsx +114 -57
- package/src/components/NavManager.tsx +42 -43
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +57 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/booking/BookingFlow.astro +150 -0
- package/src/components/booking/HoursEditor.astro +63 -0
- package/src/components/builder/PageBuilder.tsx +10 -2
- package/src/components/editor/EventPanel.tsx +401 -0
- package/src/components/editor/FormatDock.tsx +81 -0
- package/src/components/editor/LivePane.tsx +122 -0
- package/src/components/editor/MobileToolbar.tsx +93 -0
- package/src/components/editor/PostEditor.tsx +605 -223
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/slash.tsx +7 -1
- package/src/components/site/Pager.astro +24 -0
- package/src/components/site/PostCard.astro +64 -0
- package/src/components/site/TagLabel.astro +11 -0
- package/src/layouts/Admin.astro +289 -49
- package/src/layouts/Site.astro +324 -59
- package/src/lib/agent-api.ts +13 -0
- package/src/lib/agent-discovery.ts +40 -0
- package/src/lib/agent.ts +114 -0
- package/src/lib/ai-config.ts +58 -0
- package/src/lib/ai-schema.ts +22 -0
- package/src/lib/ai.ts +90 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +52 -0
- package/src/lib/auth.ts +25 -5
- package/src/lib/backup/content.ts +36 -0
- package/src/lib/backup/unzip.ts +12 -1
- package/src/lib/backup/zip.ts +1 -1
- package/src/lib/booking/agent.ts +108 -0
- package/src/lib/booking/availability.ts +221 -0
- package/src/lib/booking/emails.ts +115 -0
- package/src/lib/booking/forms.ts +77 -0
- package/src/lib/booking/service.ts +277 -0
- package/src/lib/booking/view.ts +54 -0
- package/src/lib/calendar/agent-schema.ts +48 -0
- package/src/lib/calendar/agent.ts +136 -0
- package/src/lib/calendar/api.ts +81 -0
- package/src/lib/calendar/cards.ts +102 -0
- package/src/lib/calendar/config.ts +52 -0
- package/src/lib/calendar/data.ts +130 -0
- package/src/lib/calendar/events.ts +261 -0
- package/src/lib/calendar/feeds.ts +43 -0
- package/src/lib/calendar/hooks.ts +38 -0
- package/src/lib/calendar/http.ts +30 -0
- package/src/lib/calendar/ics-write.ts +120 -0
- package/src/lib/calendar/ics.ts +15 -5
- package/src/lib/calendar/import.ts +203 -0
- package/src/lib/calendar/render.ts +120 -37
- package/src/lib/calendar/rrule.ts +149 -0
- package/src/lib/calendar/seo.ts +48 -0
- package/src/lib/design-service.ts +58 -0
- package/src/lib/designDiff.ts +145 -0
- package/src/lib/designPreview.ts +110 -12
- package/src/lib/documents/cleanup.ts +31 -0
- package/src/lib/documents/extract.ts +154 -0
- package/src/lib/documents/vendor.d.ts +4 -0
- package/src/lib/drafts.ts +60 -6
- package/src/lib/dynamicBlocks.ts +40 -8
- package/src/lib/email/accounts.ts +55 -0
- package/src/lib/email/config.ts +86 -0
- package/src/lib/email/migration.ts +24 -0
- package/src/lib/email/providers.ts +89 -0
- package/src/lib/email/service.ts +78 -0
- package/src/lib/email/store.ts +167 -0
- package/src/lib/email/templates.ts +18 -0
- package/src/lib/email/types.ts +32 -0
- package/src/lib/email/webhooks.ts +87 -0
- package/src/lib/email.ts +15 -0
- package/src/lib/env.ts +1 -1
- package/src/lib/legal/index.ts +25 -0
- package/src/lib/mobile/bookings.ts +73 -0
- package/src/lib/mobile/calendar.ts +85 -0
- package/src/lib/mobile/content.ts +85 -0
- package/src/lib/mobile/http.ts +63 -0
- package/src/lib/mobile/route.ts +20 -0
- package/src/lib/ops.ts +8 -0
- package/src/lib/preview.ts +21 -1
- package/src/lib/repo/migrations.ts +217 -0
- package/src/lib/repo/sql.ts +853 -60
- package/src/lib/repo/types.ts +122 -5
- package/src/lib/scheduler.ts +25 -0
- package/src/lib/schema.ts +309 -0
- package/src/lib/search-i18n.ts +3 -0
- package/src/lib/socialIcons.ts +32 -0
- package/src/lib/tagColor.ts +11 -0
- package/src/lib/theme.ts +59 -40
- package/src/lib/themeTokens.ts +174 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhooks.ts +139 -0
- package/src/middleware.ts +36 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +8 -5
- package/src/pages/account/[action].astro +53 -0
- package/src/pages/admin/ai.astro +12 -0
- package/src/pages/admin/c/[collection]/[id].astro +4 -1
- package/src/pages/admin/c/[collection]/index.astro +127 -92
- package/src/pages/admin/calendar/bookings.astro +400 -0
- package/src/pages/admin/calendar/calendars.astro +265 -0
- package/src/pages/admin/calendar/events/[id].astro +56 -0
- package/src/pages/admin/calendar/events/index.astro +408 -0
- package/src/pages/admin/calendar/index.ts +4 -0
- package/src/pages/admin/calendar/integrations.astro +221 -0
- package/src/pages/admin/calendar/resources/[id].astro +180 -0
- package/src/pages/admin/calendar/resources/index.astro +106 -0
- package/src/pages/admin/calendar/resources/link.astro +196 -0
- package/src/pages/admin/design.astro +10 -3
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +232 -85
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +4 -1
- package/src/pages/admin/pages/index.astro +41 -48
- package/src/pages/admin/settings.astro +418 -367
- package/src/pages/admin/tags.astro +4 -4
- package/src/pages/admin/users.astro +38 -28
- package/src/pages/api/agent/docs.ts +33 -0
- package/src/pages/api/agent/index.ts +21 -0
- package/src/pages/api/ai/clean.ts +13 -0
- package/src/pages/api/ai/draft.ts +14 -0
- package/src/pages/api/ai/settings.ts +53 -0
- package/src/pages/api/ai/test.ts +12 -0
- package/src/pages/api/backup.ts +8 -21
- package/src/pages/api/calendar/availability.ts +35 -0
- package/src/pages/api/calendar/bookings/[id].ts +49 -0
- package/src/pages/api/calendar/bookings/index.ts +73 -0
- package/src/pages/api/calendar/calendars.ts +35 -0
- package/src/pages/api/calendar/docs.ts +64 -0
- package/src/pages/api/calendar/events/[id].ts +63 -0
- package/src/pages/api/calendar/events/index.ts +112 -0
- package/src/pages/api/calendar/resources.ts +34 -0
- package/src/pages/api/design/capabilities.ts +10 -0
- package/src/pages/api/design/index.ts +58 -0
- package/src/pages/api/design/keys.ts +24 -0
- package/src/pages/api/design/media.ts +32 -0
- package/src/pages/api/email/run.ts +11 -0
- package/src/pages/api/email/settings.ts +61 -0
- package/src/pages/api/email/webhooks/[provider].ts +4 -0
- package/src/pages/api/mcp.ts +59 -0
- package/src/pages/api/media/upload.ts +2 -2
- package/src/pages/api/ops/status.ts +39 -0
- package/src/pages/api/restore.ts +27 -37
- package/src/pages/api/search.ts +18 -4
- package/src/pages/api/v1/availability.ts +7 -0
- package/src/pages/api/v1/bookings.ts +17 -0
- package/src/pages/api/v1/calendars.ts +7 -0
- package/src/pages/api/v1/content/[id].ts +7 -0
- package/src/pages/api/v1/content/index.ts +7 -0
- package/src/pages/api/v1/events.ts +7 -0
- package/src/pages/api/v1/resources.ts +7 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +137 -66
- package/src/pages/blog/index.astro +95 -30
- package/src/pages/blog/tag/[slug].astro +45 -40
- package/src/pages/book/[resource]/[link].astro +70 -0
- package/src/pages/book/[resource]/index.astro +63 -0
- package/src/pages/book/[resource].ics.ts +38 -0
- package/src/pages/book/index.astro +47 -0
- package/src/pages/book/manage/[token].astro +108 -0
- package/src/pages/calendar/[slug].ics.ts +27 -0
- package/src/pages/calendar/feed.ics.ts +15 -0
- package/src/pages/calendar/index.astro +191 -0
- package/src/pages/embed/book/[resource]/[link].astro +33 -0
- package/src/pages/embed/calendar/[slug].astro +43 -0
- package/src/pages/events/[slug].astro +74 -0
- package/src/pages/events/[slug].ics.ts +25 -0
- package/src/pages/index.astro +6 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +23 -3
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +27 -6
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +924 -105
- package/src/styles/cards.css +92 -74
- package/src/styles/editor.css +492 -114
- package/src/styles/email-settings.css +117 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -281
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +80 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +52 -0
- package/src/themes/editorial/templates/home.astro +4 -4
- package/src/themes/editorial/templates/post-minimal.astro +2 -2
- package/src/themes/editorial/theme.css +22 -37
- package/src/themes/editorial/theme.json +23 -3
- package/src/themes/field/theme.json +26 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +27 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
package/src/lib/repo/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BookingRequest } from '../backup/content';
|
|
2
|
+
import type { EmailStore } from '../email/store';
|
|
3
|
+
export interface DesignVersion { id: string; createdAt: number; author: string; summary: string; snapshot: Record<string, unknown> }
|
|
4
|
+
|
|
5
|
+
import type { Page, Post, Link, Media, NavItem, Redirect, Settings, Snippet, Tag, User, Role, WidgetInstance, Calendar, CalEvent, ApiKey, ApiScope, Webhook, WebhookDelivery, Resource, BookingLink, Booking, BookingStatus } from '../schema';
|
|
2
6
|
|
|
3
7
|
export interface Stats {
|
|
4
8
|
publishedPages: number;
|
|
@@ -16,13 +20,32 @@ export interface SearchHit<T> {
|
|
|
16
20
|
export interface SearchResults {
|
|
17
21
|
posts: SearchHit<Post>[];
|
|
18
22
|
pages: SearchHit<Page>[];
|
|
23
|
+
// published events; callers still apply calendar visibility (a private
|
|
24
|
+
// calendar's event must not surface publicly)
|
|
25
|
+
events: SearchHit<CalEvent>[];
|
|
19
26
|
tags: { tag: Tag; count: number }[];
|
|
20
27
|
authors: User[];
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
export type EventUpdate = Partial<
|
|
31
|
+
Pick<
|
|
32
|
+
CalEvent,
|
|
33
|
+
| 'title' | 'slug' | 'bodyJson' | 'bodyHtml' | 'excerpt' | 'cover' | 'coverAlt'
|
|
34
|
+
| 'seoTitle' | 'seoDesc' | 'status' | 'featured' | 'tags' | 'template'
|
|
35
|
+
| 'start' | 'end' | 'allDay' | 'timezone' | 'rrule' | 'exdates' | 'cancelled'
|
|
36
|
+
| 'location' | 'cta' | 'calendarIds' | 'fields' | 'sourceUid' | 'rsvp'
|
|
37
|
+
| 'publishedAt' | 'scheduledAt'
|
|
38
|
+
>
|
|
39
|
+
>;
|
|
40
|
+
|
|
23
41
|
// The single contract both backends implement. The app only ever imports
|
|
24
42
|
// this — never a concrete adapter.
|
|
25
43
|
export interface ContentRepository {
|
|
44
|
+
readonly email: EmailStore;
|
|
45
|
+
// Bounded public catalog; fetch limit + 1 IDs without loading every body.
|
|
46
|
+
listPublicContent(options: { page: number; limit: number; kind?: 'post' | 'page'; collection?: string; locale?: string; collections: string[]; defaultLocale: string }): Promise<{ id: string; kind: 'post' | 'page' }[]>;
|
|
47
|
+
exportBookingState(): Promise<{ bookings: Booking[]; bookingRequests: BookingRequest[] }>;
|
|
48
|
+
getBookingRequest(keyHash: string): Promise<{ requestHash: string; bookingId: string } | null>;
|
|
26
49
|
// pages
|
|
27
50
|
listPages(): Promise<Page[]>;
|
|
28
51
|
getPage(id: string): Promise<Page | null>;
|
|
@@ -71,9 +94,86 @@ export interface ContentRepository {
|
|
|
71
94
|
applyPostDraft(id: string): Promise<void>;
|
|
72
95
|
discardPostDraft(id: string): Promise<void>;
|
|
73
96
|
deletePost(id: string): Promise<void>;
|
|
74
|
-
// flips scheduled posts whose time has passed to published;
|
|
97
|
+
// flips scheduled posts AND events whose time has passed to published;
|
|
98
|
+
// returns the combined count
|
|
75
99
|
publishDuePosts(now: number): Promise<number>;
|
|
76
100
|
|
|
101
|
+
// calendars — native calendars group events; feed calendars mirror an ICS
|
|
102
|
+
// URL live (their events are never stored). Deleting a calendar removes it
|
|
103
|
+
// from every event's calendarIds; the events themselves stay.
|
|
104
|
+
listCalendars(): Promise<Calendar[]>;
|
|
105
|
+
getCalendar(id: string): Promise<Calendar | null>;
|
|
106
|
+
getCalendarBySlug(slug: string): Promise<Calendar | null>;
|
|
107
|
+
createCalendar(data: Partial<Pick<Calendar, 'color' | 'description' | 'kind' | 'feedUrl' | 'visibility'>> & { name: string; slug: string }): Promise<Calendar>;
|
|
108
|
+
updateCalendar(id: string, data: Partial<Pick<Calendar, 'name' | 'slug' | 'color' | 'description' | 'feedUrl' | 'visibility' | 'feedToken'>>): Promise<void>;
|
|
109
|
+
reorderCalendars(ids: string[]): Promise<void>;
|
|
110
|
+
deleteCalendar(id: string): Promise<void>;
|
|
111
|
+
|
|
112
|
+
// events — same draft/publish contract as posts (live events stage edits
|
|
113
|
+
// until Update). startAt/endAt are derived from the wall-clock fields and
|
|
114
|
+
// the event (or site) timezone on every write.
|
|
115
|
+
listEvents(): Promise<CalEvent[]>;
|
|
116
|
+
listPublishedEvents(): Promise<CalEvent[]>;
|
|
117
|
+
getEvent(id: string): Promise<CalEvent | null>;
|
|
118
|
+
getEventBySlug(slug: string): Promise<CalEvent | null>;
|
|
119
|
+
createEvent(data: Partial<Omit<CalEvent, 'id' | 'draft' | 'startAt' | 'endAt' | 'createdAt' | 'updatedAt'>> & { title: string; slug: string; start: string }): Promise<CalEvent>;
|
|
120
|
+
updateEvent(id: string, data: EventUpdate): Promise<void>;
|
|
121
|
+
saveEventDraft(id: string, data: Omit<EventUpdate, 'status' | 'scheduledAt' | 'publishedAt'>): Promise<void>;
|
|
122
|
+
applyEventDraft(id: string): Promise<void>;
|
|
123
|
+
discardEventDraft(id: string): Promise<void>;
|
|
124
|
+
deleteEvent(id: string): Promise<void>;
|
|
125
|
+
|
|
126
|
+
// resources (rooms, people, vans…) and their booking links. Deleting a
|
|
127
|
+
// resource deletes its links; its bookings stay as history.
|
|
128
|
+
listResources(): Promise<Resource[]>;
|
|
129
|
+
getResource(id: string): Promise<Resource | null>;
|
|
130
|
+
getResourceBySlug(slug: string): Promise<Resource | null>;
|
|
131
|
+
createResource(data: Partial<Omit<Resource, 'id' | 'createdAt' | 'updatedAt' | 'order' | 'feedToken'>> & { name: string; slug: string }): Promise<Resource>;
|
|
132
|
+
updateResource(id: string, data: Partial<Omit<Resource, 'id' | 'createdAt' | 'updatedAt' | 'order'>>): Promise<void>;
|
|
133
|
+
reorderResources(ids: string[]): Promise<void>;
|
|
134
|
+
deleteResource(id: string): Promise<void>;
|
|
135
|
+
listBookingLinks(resourceId?: string): Promise<BookingLink[]>;
|
|
136
|
+
getBookingLink(id: string): Promise<BookingLink | null>;
|
|
137
|
+
getBookingLinkBySlug(resourceId: string, slug: string): Promise<BookingLink | null>;
|
|
138
|
+
createBookingLink(data: Partial<Omit<BookingLink, 'id' | 'createdAt' | 'updatedAt' | 'order'>> & { resourceId: string; name: string; slug: string }): Promise<BookingLink>;
|
|
139
|
+
updateBookingLink(id: string, data: Partial<Omit<BookingLink, 'id' | 'resourceId' | 'createdAt' | 'updatedAt'>>): Promise<void>;
|
|
140
|
+
deleteBookingLink(id: string): Promise<void>;
|
|
141
|
+
|
|
142
|
+
// bookings (+ staff blocks and event RSVPs). createBooking is the only
|
|
143
|
+
// write that can fail on a race: it inserts atomically only if the held
|
|
144
|
+
// range is still free (resource bookings) or seats remain (RSVPs), and
|
|
145
|
+
// returns null when it lost.
|
|
146
|
+
listBookings(filter?: { resourceId?: string; eventId?: string; from?: number; to?: number; statuses?: BookingStatus[]; kinds?: Booking['kind'][] }): Promise<Booking[]>;
|
|
147
|
+
getBooking(id: string): Promise<Booking | null>;
|
|
148
|
+
getBookingByToken(token: string): Promise<Booking | null>;
|
|
149
|
+
createBooking(
|
|
150
|
+
data: Omit<Booking, 'id' | 'createdAt' | 'updatedAt' | 'reminderSentAt' | 'notificationVersion'>,
|
|
151
|
+
guard?: { capacity?: number; idempotency?: { keyHash: string; requestHash: string } }
|
|
152
|
+
): Promise<Booking | null>;
|
|
153
|
+
// moves a booking only if the new held range is free (reschedule)
|
|
154
|
+
moveBooking(id: string, times: { start: number; end: number; holdStart: number; holdEnd: number }): Promise<boolean>;
|
|
155
|
+
updateBooking(id: string, data: Partial<Pick<Booking, 'status' | 'notes' | 'name' | 'email' | 'phone' | 'answers' | 'reminderSentAt'>> & { notificationMessage?: string }): Promise<void>;
|
|
156
|
+
deleteBooking(id: string): Promise<void>;
|
|
157
|
+
// confirmed bookings starting in [from, to) that haven't had a reminder
|
|
158
|
+
listDueReminders(from: number, to: number): Promise<Booking[]>;
|
|
159
|
+
|
|
160
|
+
// scoped API keys — only the SHA-256 hash is stored; findApiKeyByHash
|
|
161
|
+
// also stamps lastUsedAt
|
|
162
|
+
listApiKeys(): Promise<ApiKey[]>;
|
|
163
|
+
createApiKey(data: { name: string; prefix: string; keyHash: string; scopes: ApiScope[] }): Promise<ApiKey>;
|
|
164
|
+
findApiKeyByHash(keyHash: string): Promise<ApiKey | null>;
|
|
165
|
+
deleteApiKey(id: string): Promise<void>;
|
|
166
|
+
|
|
167
|
+
// webhooks + their delivery queue (lib/webhooks.ts drives both)
|
|
168
|
+
listWebhooks(): Promise<Webhook[]>;
|
|
169
|
+
createWebhook(data: { url: string; secret: string; events: string[] }): Promise<Webhook>;
|
|
170
|
+
updateWebhook(id: string, data: Partial<Pick<Webhook, 'url' | 'events' | 'active' | 'secret'>>): Promise<void>;
|
|
171
|
+
deleteWebhook(id: string): Promise<void>;
|
|
172
|
+
createDeliveries(rows: Omit<WebhookDelivery, 'attempts' | 'responseCode' | 'deliveredAt'>[]): Promise<void>;
|
|
173
|
+
listDueDeliveries(now: number, limit: number): Promise<WebhookDelivery[]>;
|
|
174
|
+
listDeliveries(webhookId: string, limit: number): Promise<WebhookDelivery[]>;
|
|
175
|
+
recordDelivery(id: string, data: Pick<WebhookDelivery, 'status' | 'attempts' | 'nextAt' | 'responseCode' | 'deliveredAt'>): Promise<void>;
|
|
176
|
+
|
|
77
177
|
// media
|
|
78
178
|
listMedia(): Promise<Media[]>;
|
|
79
179
|
getMedia(id: string): Promise<Media | null>;
|
|
@@ -119,7 +219,7 @@ export interface ContentRepository {
|
|
|
119
219
|
createUser(data: { name: string; email: string; role: Role; passHash: string }): Promise<User>;
|
|
120
220
|
updateUser(id: string, data: { name?: string; email?: string; role?: Role; passHash?: string; bio?: string; avatar?: string; slug?: string }): Promise<void>;
|
|
121
221
|
deleteUser(id: string): Promise<void>;
|
|
122
|
-
getUserAuth(email: string): Promise<{ user: User; passHash: string } | null>;
|
|
222
|
+
getUserAuth(email: string): Promise<{ user: User; passHash: string; sessionVersion: number } | null>;
|
|
123
223
|
|
|
124
224
|
// navigation — managed header/footer/social items; empty header falls back
|
|
125
225
|
// to the legacy auto-list of published pages in Site.astro
|
|
@@ -148,6 +248,13 @@ export interface ContentRepository {
|
|
|
148
248
|
navItems: NavItem[];
|
|
149
249
|
users: User[];
|
|
150
250
|
redirects: Redirect[];
|
|
251
|
+
// added with the calendar module; older backups restore with none
|
|
252
|
+
calendars?: Calendar[];
|
|
253
|
+
events?: CalEvent[];
|
|
254
|
+
resources?: Resource[];
|
|
255
|
+
bookingLinks?: BookingLink[];
|
|
256
|
+
bookings?: Booking[];
|
|
257
|
+
bookingRequests?: BookingRequest[];
|
|
151
258
|
}): Promise<void>;
|
|
152
259
|
|
|
153
260
|
// redirects — exact-path forwards, consulted when a request would 404.
|
|
@@ -161,8 +268,18 @@ export interface ContentRepository {
|
|
|
161
268
|
updateSettings(data: Partial<Settings>): Promise<void>;
|
|
162
269
|
// Design-screen staging: save stages into settings.draftDesign, apply
|
|
163
270
|
// copies staged → live design fields and clears, discard just clears
|
|
164
|
-
saveDesignDraft(data: Partial<Pick<Settings, 'activeTheme' | 'designOverrides' | 'siteLogo' | 'siteFavicon' | 'siteCover'>>
|
|
165
|
-
|
|
271
|
+
saveDesignDraft(data: Partial<Pick<Settings, 'activeTheme' | 'designOverrides' | 'siteLogo' | 'siteFavicon' | 'siteCover' | 'sectionPages'>> & {
|
|
272
|
+
navItems?: { id: string; label: string; url: string; area: NavItem['area'] }[];
|
|
273
|
+
shareEpoch?: string;
|
|
274
|
+
}): Promise<void>;
|
|
275
|
+
// replace the whole staged draft (null clears it) — per-change undo, restore
|
|
276
|
+
setDesignDraft(draft: Record<string, unknown> | null, expectedRevision?: number, actor?: string): Promise<void>;
|
|
277
|
+
listDesignAudit(): Promise<Record<string, unknown>[]>;
|
|
278
|
+
// publish: snapshot the outgoing live design into design_versions (last 20
|
|
279
|
+
// kept), apply the draft's settings + staged navigation, clear the draft
|
|
280
|
+
applyDesignDraft(opts?: { author?: string; summary?: string; expectedRevision?: number }): Promise<void>;
|
|
166
281
|
discardDesignDraft(): Promise<void>;
|
|
282
|
+
listDesignVersions(limit?: number): Promise<DesignVersion[]>;
|
|
283
|
+
getDesignVersion(id: string): Promise<DesignVersion | null>;
|
|
167
284
|
getStats(): Promise<Stats>;
|
|
168
285
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ContentRepository } from './repo/types';
|
|
2
|
+
import { processDueDeliveries } from './webhooks';
|
|
3
|
+
import { dispatchEmail } from './email/service';
|
|
4
|
+
import { sendDueReminders } from './booking/service';
|
|
5
|
+
|
|
6
|
+
// All work is awaited on both hosts. Traffic-triggered fallbacks do not mark
|
|
7
|
+
// this heartbeat: monitoring must prove the independent scheduler ran.
|
|
8
|
+
export async function runScheduledTasks(repo: ContentRepository, locals?: unknown) {
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
const settings = await repo.getSettings();
|
|
11
|
+
const jobs = {
|
|
12
|
+
publish: () => repo.publishDuePosts(startedAt),
|
|
13
|
+
webhooks: () => processDueDeliveries(repo),
|
|
14
|
+
reminders: () => sendDueReminders(repo, { origin: settings.siteUrl, locals, now: startedAt }),
|
|
15
|
+
email: () => dispatchEmail(repo, locals, 10),
|
|
16
|
+
};
|
|
17
|
+
const results: Record<string, { ok: boolean; count?: number }> = {};
|
|
18
|
+
for (const [name, run] of Object.entries(jobs)) {
|
|
19
|
+
try { results[name] = { ok: true, count: await run() }; }
|
|
20
|
+
catch { results[name] = { ok: false }; }
|
|
21
|
+
}
|
|
22
|
+
const report = { startedAt, finishedAt: Date.now(), ok: Object.values(results).every(r => r.ok), jobs: results };
|
|
23
|
+
await repo.email.recordScheduler(report);
|
|
24
|
+
return report;
|
|
25
|
+
}
|
package/src/lib/schema.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SectionPages } from '../theme-sdk/contract.mjs';
|
|
2
|
+
import { AiConfig, AgentConfig } from './ai-schema';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
|
|
3
5
|
// ---- Widgets -------------------------------------------------------------
|
|
@@ -236,6 +238,309 @@ export const Redirect = z.object({
|
|
|
236
238
|
});
|
|
237
239
|
export type Redirect = z.infer<typeof Redirect>;
|
|
238
240
|
|
|
241
|
+
// ---- Calendar ------------------------------------------------------------
|
|
242
|
+
// Calendars group events. `native` calendars hold events authored in the
|
|
243
|
+
// admin; `feed` calendars mirror an external ICS feed (Google, Apple,
|
|
244
|
+
// Outlook, any .ics) live, read-only. Visibility decides where a calendar's
|
|
245
|
+
// events surface: `public` = listed on the calendar page + the all-calendars
|
|
246
|
+
// feed; `unlisted` = event pages and its own feed work, but it's not listed;
|
|
247
|
+
// `private` = admin-only, plus a secret-token feed for staff subscriptions.
|
|
248
|
+
export const CalendarKind = z.enum(['native', 'feed']);
|
|
249
|
+
export type CalendarKind = z.infer<typeof CalendarKind>;
|
|
250
|
+
export const CalendarVisibility = z.enum(['public', 'unlisted', 'private']);
|
|
251
|
+
export type CalendarVisibility = z.infer<typeof CalendarVisibility>;
|
|
252
|
+
|
|
253
|
+
export const Calendar = z.object({
|
|
254
|
+
id: z.string(),
|
|
255
|
+
slug: z.string(),
|
|
256
|
+
name: z.string(),
|
|
257
|
+
color: z.string().default('#4f46e5'),
|
|
258
|
+
description: z.string().default(''),
|
|
259
|
+
kind: CalendarKind.default('native'),
|
|
260
|
+
// feed calendars only: the normalized ICS URL
|
|
261
|
+
feedUrl: z.string().default(''),
|
|
262
|
+
visibility: CalendarVisibility.default('public'),
|
|
263
|
+
// secret for /calendar/<slug>.ics?t=… on private calendars (rotatable)
|
|
264
|
+
feedToken: z.string().default(''),
|
|
265
|
+
order: z.number().default(0),
|
|
266
|
+
createdAt: z.number(),
|
|
267
|
+
updatedAt: z.number(),
|
|
268
|
+
});
|
|
269
|
+
export type Calendar = z.infer<typeof Calendar>;
|
|
270
|
+
|
|
271
|
+
// Times are stored as WALL-CLOCK strings in the event's timezone — that's
|
|
272
|
+
// what recurrence is defined against (a 10am weekly service stays at 10am
|
|
273
|
+
// across DST). Timed: 'YYYY-MM-DDTHH:mm'. All-day: 'YYYY-MM-DD', with `end`
|
|
274
|
+
// the INCLUSIVE last day (how people think about it; ICS output converts to
|
|
275
|
+
// the exclusive form). `timezone` '' = the site timezone. startAt/endAt are
|
|
276
|
+
// the derived UTC instants of the first occurrence, kept for sorting.
|
|
277
|
+
export const EventLocation = z.object({
|
|
278
|
+
name: z.string().default(''),
|
|
279
|
+
address: z.string().default(''),
|
|
280
|
+
// a map link or an online-meeting URL
|
|
281
|
+
url: z.string().default(''),
|
|
282
|
+
});
|
|
283
|
+
export type EventLocation = z.infer<typeof EventLocation>;
|
|
284
|
+
|
|
285
|
+
export const EventCta = z.object({
|
|
286
|
+
label: z.string().default(''),
|
|
287
|
+
href: z.string().default(''),
|
|
288
|
+
style: z.enum(['primary', 'outline']).default('primary'),
|
|
289
|
+
});
|
|
290
|
+
export type EventCta = z.infer<typeof EventCta>;
|
|
291
|
+
|
|
292
|
+
export const CalEvent = z.object({
|
|
293
|
+
id: z.string(),
|
|
294
|
+
slug: z.string(),
|
|
295
|
+
title: z.string(),
|
|
296
|
+
bodyJson: z.string().default(''),
|
|
297
|
+
bodyHtml: z.string().default(''),
|
|
298
|
+
excerpt: z.string().default(''),
|
|
299
|
+
cover: z.string().default(''),
|
|
300
|
+
coverAlt: z.string().default(''),
|
|
301
|
+
seoTitle: z.string().default(''),
|
|
302
|
+
seoDesc: z.string().default(''),
|
|
303
|
+
status: Status,
|
|
304
|
+
featured: z.boolean().default(false),
|
|
305
|
+
tags: z.array(z.string()).default([]),
|
|
306
|
+
template: z.string().default(''),
|
|
307
|
+
start: z.string(),
|
|
308
|
+
end: z.string().default(''),
|
|
309
|
+
allDay: z.boolean().default(false),
|
|
310
|
+
timezone: z.string().default(''),
|
|
311
|
+
// RFC 5545 RRULE body without the "RRULE:" prefix; '' = one-off
|
|
312
|
+
rrule: z.string().default(''),
|
|
313
|
+
// wall-clock starts (same format as `start`) of skipped occurrences
|
|
314
|
+
exdates: z.array(z.string()).default([]),
|
|
315
|
+
// a cancelled event stays visible (struck through) so people aren't left
|
|
316
|
+
// wondering where it went; feeds emit STATUS:CANCELLED
|
|
317
|
+
cancelled: z.boolean().default(false),
|
|
318
|
+
location: EventLocation.default({}),
|
|
319
|
+
cta: EventCta.default({}),
|
|
320
|
+
calendarIds: z.array(z.string()).default([]),
|
|
321
|
+
// values for acms({ calendar.eventFields }) custom fields, keyed by field key
|
|
322
|
+
fields: z.record(z.string()).default({}),
|
|
323
|
+
// UID of the feed event this was imported from ('' = authored here)
|
|
324
|
+
sourceUid: z.string().default(''),
|
|
325
|
+
// RSVP: seats per occurrence (0 = unlimited) and whether RSVPs confirm at
|
|
326
|
+
// once; RSVPs are stored as bookings of kind 'rsvp'
|
|
327
|
+
rsvp: z.object({
|
|
328
|
+
enabled: z.boolean().default(false),
|
|
329
|
+
capacity: z.number().int().default(0),
|
|
330
|
+
autoConfirm: z.boolean().default(true),
|
|
331
|
+
maxSeats: z.number().int().default(4),
|
|
332
|
+
}).default({}),
|
|
333
|
+
startAt: z.number(),
|
|
334
|
+
endAt: z.number(),
|
|
335
|
+
draft: z.record(z.any()).nullable().default(null),
|
|
336
|
+
publishedAt: z.number().nullable().default(null),
|
|
337
|
+
scheduledAt: z.number().nullable().default(null),
|
|
338
|
+
createdAt: z.number(),
|
|
339
|
+
updatedAt: z.number(),
|
|
340
|
+
});
|
|
341
|
+
export type CalEvent = z.infer<typeof CalEvent>;
|
|
342
|
+
|
|
343
|
+
// ---- Bookings --------------------------------------------------------------
|
|
344
|
+
// A Resource is anything people reserve: a room, a person's time, a van. Its
|
|
345
|
+
// weekly hours + date overrides are the outer bound; each BookingLink narrows
|
|
346
|
+
// them and adds its own rules (length, buffers, notice, questions, whether it
|
|
347
|
+
// confirms automatically). Every link of a resource subtracts the same
|
|
348
|
+
// Bookings, so links can never double-book the resource. Times: weekly hours
|
|
349
|
+
// and overrides are wall-clock in the resource's zone; bookings store UTC.
|
|
350
|
+
|
|
351
|
+
export const TimeRange = z.object({
|
|
352
|
+
start: z.string().regex(/^\d{2}:\d{2}$/),
|
|
353
|
+
end: z.string().regex(/^\d{2}:\d{2}$/),
|
|
354
|
+
});
|
|
355
|
+
export type TimeRange = z.infer<typeof TimeRange>;
|
|
356
|
+
|
|
357
|
+
export const WEEKDAYS = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'] as const;
|
|
358
|
+
export type Weekday = (typeof WEEKDAYS)[number];
|
|
359
|
+
export const WeeklyHours = z.object({
|
|
360
|
+
su: z.array(TimeRange).default([]),
|
|
361
|
+
mo: z.array(TimeRange).default([]),
|
|
362
|
+
tu: z.array(TimeRange).default([]),
|
|
363
|
+
we: z.array(TimeRange).default([]),
|
|
364
|
+
th: z.array(TimeRange).default([]),
|
|
365
|
+
fr: z.array(TimeRange).default([]),
|
|
366
|
+
sa: z.array(TimeRange).default([]),
|
|
367
|
+
});
|
|
368
|
+
export type WeeklyHours = z.infer<typeof WeeklyHours>;
|
|
369
|
+
|
|
370
|
+
// a date (or inclusive date span) with replacement hours; [] = closed
|
|
371
|
+
export const DateOverride = z.object({
|
|
372
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
373
|
+
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).default(''),
|
|
374
|
+
ranges: z.array(TimeRange).default([]),
|
|
375
|
+
label: z.string().default(''),
|
|
376
|
+
});
|
|
377
|
+
export type DateOverride = z.infer<typeof DateOverride>;
|
|
378
|
+
|
|
379
|
+
export const Resource = z.object({
|
|
380
|
+
id: z.string(),
|
|
381
|
+
slug: z.string(),
|
|
382
|
+
name: z.string(),
|
|
383
|
+
// a free label: Room, Person, Vehicle, Equipment… (acms({ calendar.resourceKinds }) suggests values)
|
|
384
|
+
kind: z.string().default('Room'),
|
|
385
|
+
description: z.string().default(''),
|
|
386
|
+
photo: z.string().default(''),
|
|
387
|
+
capacity: z.number().int().nullable().default(null),
|
|
388
|
+
amenities: z.array(z.string()).default([]),
|
|
389
|
+
timezone: z.string().default(''),
|
|
390
|
+
hours: WeeklyHours.default({}),
|
|
391
|
+
overrides: z.array(DateOverride).default([]),
|
|
392
|
+
// external ICS feeds whose events block time (a person's Google calendar)
|
|
393
|
+
busyFeeds: z.array(z.string()).default([]),
|
|
394
|
+
notifyEmails: z.array(z.string()).default([]),
|
|
395
|
+
// public = listed in /book; unlisted = reachable by link only
|
|
396
|
+
visibility: z.enum(['public', 'unlisted']).default('public'),
|
|
397
|
+
// confirmed bookings show on the public calendar as "Reserved" blocks
|
|
398
|
+
showOnCalendar: z.boolean().default(false),
|
|
399
|
+
// secret for the staff feed /book/<slug>.ics?t=… (bookings with names)
|
|
400
|
+
feedToken: z.string().default(''),
|
|
401
|
+
order: z.number().default(0),
|
|
402
|
+
createdAt: z.number(),
|
|
403
|
+
updatedAt: z.number(),
|
|
404
|
+
});
|
|
405
|
+
export type Resource = z.infer<typeof Resource>;
|
|
406
|
+
|
|
407
|
+
export const QUESTION_TYPES = ['text', 'textarea', 'email', 'phone', 'number', 'select', 'checkbox', 'date'] as const;
|
|
408
|
+
export const BookingQuestion = z.object({
|
|
409
|
+
id: z.string(),
|
|
410
|
+
label: z.string(),
|
|
411
|
+
type: z.enum(QUESTION_TYPES).default('text'),
|
|
412
|
+
required: z.boolean().default(false),
|
|
413
|
+
options: z.array(z.string()).default([]),
|
|
414
|
+
help: z.string().default(''),
|
|
415
|
+
});
|
|
416
|
+
export type BookingQuestion = z.infer<typeof BookingQuestion>;
|
|
417
|
+
|
|
418
|
+
export const BookingMode = z.enum(['slots', 'range', 'day']);
|
|
419
|
+
export type BookingMode = z.infer<typeof BookingMode>;
|
|
420
|
+
|
|
421
|
+
export const BookingLink = z.object({
|
|
422
|
+
id: z.string(),
|
|
423
|
+
resourceId: z.string(),
|
|
424
|
+
slug: z.string(),
|
|
425
|
+
name: z.string(),
|
|
426
|
+
description: z.string().default(''),
|
|
427
|
+
// slots: fixed length; range: choose a length from `lengths`; day: whole days
|
|
428
|
+
mode: BookingMode.default('slots'),
|
|
429
|
+
duration: z.number().int().default(60), // minutes (slots)
|
|
430
|
+
lengths: z.array(z.number().int()).default([]), // minutes (range) — chips
|
|
431
|
+
minDays: z.number().int().default(1), // day mode
|
|
432
|
+
maxDays: z.number().int().default(1),
|
|
433
|
+
step: z.number().int().default(30), // minutes between start times
|
|
434
|
+
bufferBefore: z.number().int().default(0),
|
|
435
|
+
bufferAfter: z.number().int().default(0),
|
|
436
|
+
minNotice: z.number().int().default(120), // minutes
|
|
437
|
+
maxAdvance: z.number().int().default(90), // days
|
|
438
|
+
perDayCap: z.number().int().default(0), // 0 = unlimited
|
|
439
|
+
// narrows the resource's hours; null = use the resource's hours as-is
|
|
440
|
+
hours: WeeklyHours.nullable().default(null),
|
|
441
|
+
// confirmed at once (and on the calendar) vs. held as pending for approval
|
|
442
|
+
autoConfirm: z.boolean().default(true),
|
|
443
|
+
visibility: z.enum(['public', 'unlisted']).default('public'),
|
|
444
|
+
askPhone: z.boolean().default(false),
|
|
445
|
+
questions: z.array(BookingQuestion).default([]),
|
|
446
|
+
confirmMessage: z.string().default(''),
|
|
447
|
+
redirectUrl: z.string().default(''),
|
|
448
|
+
active: z.boolean().default(true),
|
|
449
|
+
order: z.number().default(0),
|
|
450
|
+
createdAt: z.number(),
|
|
451
|
+
updatedAt: z.number(),
|
|
452
|
+
});
|
|
453
|
+
export type BookingLink = z.infer<typeof BookingLink>;
|
|
454
|
+
|
|
455
|
+
export const BookingStatus = z.enum(['pending', 'confirmed', 'declined', 'cancelled']);
|
|
456
|
+
export type BookingStatus = z.infer<typeof BookingStatus>;
|
|
457
|
+
|
|
458
|
+
export const Booking = z.object({
|
|
459
|
+
id: z.string(),
|
|
460
|
+
// booking = someone reserved it; block = staff closed the time; rsvp = a
|
|
461
|
+
// seat at an event occurrence (resourceId/linkId empty, eventId set)
|
|
462
|
+
kind: z.enum(['booking', 'block', 'rsvp']).default('booking'),
|
|
463
|
+
resourceId: z.string().default(''),
|
|
464
|
+
linkId: z.string().default(''),
|
|
465
|
+
eventId: z.string().default(''),
|
|
466
|
+
status: BookingStatus,
|
|
467
|
+
start: z.number(),
|
|
468
|
+
end: z.number(),
|
|
469
|
+
// start/end widened by the link's buffers — what actually blocks the resource
|
|
470
|
+
holdStart: z.number(),
|
|
471
|
+
holdEnd: z.number(),
|
|
472
|
+
timezone: z.string().default(''),
|
|
473
|
+
name: z.string().default(''),
|
|
474
|
+
email: z.string().default(''),
|
|
475
|
+
phone: z.string().default(''),
|
|
476
|
+
seats: z.number().int().default(1),
|
|
477
|
+
answers: z.record(z.string()).default({}),
|
|
478
|
+
notes: z.string().default(''), // staff-only
|
|
479
|
+
manageToken: z.string().default(''),
|
|
480
|
+
source: z.enum(['web', 'admin', 'api', 'agent']).default('web'),
|
|
481
|
+
reminderSentAt: z.number().nullable().default(null),
|
|
482
|
+
notificationVersion: z.number().default(0),
|
|
483
|
+
createdAt: z.number(),
|
|
484
|
+
updatedAt: z.number(),
|
|
485
|
+
});
|
|
486
|
+
export type Booking = z.infer<typeof Booking>;
|
|
487
|
+
|
|
488
|
+
// ---- Integrations: scoped API keys + signed webhooks ----------------------
|
|
489
|
+
// Keys are shown once at creation; only a SHA-256 hash is stored. The agent
|
|
490
|
+
// key from Settings → AI & agents keeps working and counts as every scope.
|
|
491
|
+
export const API_SCOPES = [
|
|
492
|
+
'events:read', 'events:write',
|
|
493
|
+
'bookings:read', 'bookings:write',
|
|
494
|
+
'resources:read', 'resources:write',
|
|
495
|
+
'design:read', 'design:write-draft', 'design:publish', 'media:read', 'media:write',
|
|
496
|
+
] as const;
|
|
497
|
+
export const ApiScope = z.enum(API_SCOPES);
|
|
498
|
+
export type ApiScope = z.infer<typeof ApiScope>;
|
|
499
|
+
|
|
500
|
+
export const ApiKey = z.object({
|
|
501
|
+
id: z.string(),
|
|
502
|
+
name: z.string(),
|
|
503
|
+
// first characters of the key, shown in lists so owners can tell keys apart
|
|
504
|
+
prefix: z.string(),
|
|
505
|
+
scopes: z.array(ApiScope).default([]),
|
|
506
|
+
lastUsedAt: z.number().nullable().default(null),
|
|
507
|
+
createdAt: z.number(),
|
|
508
|
+
});
|
|
509
|
+
export type ApiKey = z.infer<typeof ApiKey>;
|
|
510
|
+
|
|
511
|
+
export const WEBHOOK_EVENTS = [
|
|
512
|
+
'event.published', 'event.updated', 'event.unpublished', 'event.deleted',
|
|
513
|
+
'booking.created', 'booking.confirmed', 'booking.declined', 'booking.cancelled', 'booking.rescheduled',
|
|
514
|
+
] as const;
|
|
515
|
+
export type WebhookEventType = (typeof WEBHOOK_EVENTS)[number];
|
|
516
|
+
|
|
517
|
+
export const Webhook = z.object({
|
|
518
|
+
id: z.string(),
|
|
519
|
+
url: z.string(),
|
|
520
|
+
// HMAC-SHA256 signing secret (X-Acms-Signature)
|
|
521
|
+
secret: z.string(),
|
|
522
|
+
events: z.array(z.string()).default([]),
|
|
523
|
+
active: z.boolean().default(true),
|
|
524
|
+
lastStatus: z.number().nullable().default(null),
|
|
525
|
+
lastAt: z.number().nullable().default(null),
|
|
526
|
+
createdAt: z.number(),
|
|
527
|
+
});
|
|
528
|
+
export type Webhook = z.infer<typeof Webhook>;
|
|
529
|
+
|
|
530
|
+
export const WebhookDelivery = z.object({
|
|
531
|
+
id: z.string(),
|
|
532
|
+
webhookId: z.string(),
|
|
533
|
+
event: z.string(),
|
|
534
|
+
payload: z.string(),
|
|
535
|
+
status: z.enum(['pending', 'delivered', 'failed']),
|
|
536
|
+
attempts: z.number().default(0),
|
|
537
|
+
nextAt: z.number().nullable().default(null),
|
|
538
|
+
responseCode: z.number().nullable().default(null),
|
|
539
|
+
createdAt: z.number(),
|
|
540
|
+
deliveredAt: z.number().nullable().default(null),
|
|
541
|
+
});
|
|
542
|
+
export type WebhookDelivery = z.infer<typeof WebhookDelivery>;
|
|
543
|
+
|
|
239
544
|
// normalize a redirect path: leading slash, no trailing slash, no origin
|
|
240
545
|
export function normalizeRedirectPath(input: string): string {
|
|
241
546
|
let p = input.trim();
|
|
@@ -261,6 +566,8 @@ export const Settings = z.object({
|
|
|
261
566
|
// overrides from the Design screen layered on top of the theme's defaults
|
|
262
567
|
activeTheme: z.string().default(''),
|
|
263
568
|
designOverrides: z.record(z.any()).default({}),
|
|
569
|
+
sectionPages: SectionPages.default({}),
|
|
570
|
+
designRevision: z.number().int().nonnegative().default(0),
|
|
264
571
|
siteLogo: z.string().default(''),
|
|
265
572
|
// SEO foundation: canonical origin for absolute URLs (sitemap, feeds, OG
|
|
266
573
|
// tags — empty falls back to the request origin), favicon, and the default
|
|
@@ -322,6 +629,8 @@ export const Settings = z.object({
|
|
|
322
629
|
// external drafts API: bearer key for POST /api/drafts (empty = endpoint
|
|
323
630
|
// off) and the browser origins allowed to call it ('*' entry allows all,
|
|
324
631
|
// 'http://localhost:*' matches any port). Managed in /admin/settings.
|
|
632
|
+
ai: AiConfig,
|
|
633
|
+
agent: AgentConfig,
|
|
325
634
|
draftsApiKey: z.string().default(''),
|
|
326
635
|
draftsAllowedOrigins: z.array(z.string()).default([]),
|
|
327
636
|
// staged Design-screen changes (see lib/drafts.ts); null = none pending
|
package/src/lib/search-i18n.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface SearchStrings {
|
|
|
7
7
|
placeholder: string;
|
|
8
8
|
posts: string;
|
|
9
9
|
pages: string;
|
|
10
|
+
events: string;
|
|
10
11
|
tags: string;
|
|
11
12
|
authors: string;
|
|
12
13
|
empty: string;
|
|
@@ -21,6 +22,7 @@ const STRINGS: Record<string, SearchStrings> = {
|
|
|
21
22
|
placeholder: 'Search posts, pages, tags and authors',
|
|
22
23
|
posts: 'Posts',
|
|
23
24
|
pages: 'Pages',
|
|
25
|
+
events: 'Events',
|
|
24
26
|
tags: 'Tags',
|
|
25
27
|
authors: 'Authors',
|
|
26
28
|
empty: 'Nothing found for',
|
|
@@ -33,6 +35,7 @@ const STRINGS: Record<string, SearchStrings> = {
|
|
|
33
35
|
placeholder: 'Busca artículos, páginas, etiquetas y autores',
|
|
34
36
|
posts: 'Artículos',
|
|
35
37
|
pages: 'Páginas',
|
|
38
|
+
events: 'Eventos',
|
|
36
39
|
tags: 'Etiquetas',
|
|
37
40
|
authors: 'Autores',
|
|
38
41
|
empty: 'No hay resultados para',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Brand glyphs for social links, matched by URL host. Used by the site footer
|
|
2
|
+
// and the /links profile row. Unknown hosts return null and the caller falls
|
|
3
|
+
// back to the item's text label. Paths are 24×24, filled with currentColor.
|
|
4
|
+
|
|
5
|
+
const ICONS: [RegExp, string][] = [
|
|
6
|
+
[/(^|\.)(x|twitter)\.com$/, 'M17.8 3h3.1l-6.8 7.7L22 21h-6.2l-4.9-6.4L5.3 21H2.2l7.2-8.3L2 3h6.4l4.4 5.8L17.8 3Zm-1.1 16.2h1.7L7.4 4.7H5.6l11.1 14.5Z'],
|
|
7
|
+
[/(^|\.)github\.com$/, 'M12 2.5a9.5 9.5 0 0 0-3 18.5c.5.1.7-.2.7-.5v-1.7c-2.7.6-3.2-1.2-3.2-1.2-.4-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.3 1.1 2.9.8.1-.6.3-1.1.6-1.3-2.1-.2-4.4-1.1-4.4-4.7 0-1 .4-1.9 1-2.6-.1-.2-.4-1.2.1-2.5 0 0 .8-.3 2.6 1a9 9 0 0 1 4.8 0c1.8-1.3 2.6-1 2.6-1 .5 1.3.2 2.3.1 2.5.6.7 1 1.6 1 2.6 0 3.7-2.3 4.5-4.4 4.7.3.3.6.9.6 1.8v2.6c0 .3.2.6.7.5A9.5 9.5 0 0 0 12 2.5Z'],
|
|
8
|
+
[/(^|\.)linkedin\.com$/, 'M5 3.5a2 2 0 1 1 0 4 2 2 0 0 1 0-4ZM3.3 9h3.4v11.5H3.3V9Zm5.6 0h3.3v1.6c.5-.9 1.6-1.9 3.4-1.9 3.6 0 4.2 2.3 4.2 5.4v6.4h-3.4v-5.7c0-1.4 0-3.1-1.9-3.1s-2.2 1.5-2.2 3v5.8H8.9V9Z'],
|
|
9
|
+
[/(^|\.)instagram\.com$/, 'M12 7.4a4.6 4.6 0 1 0 0 9.2 4.6 4.6 0 0 0 0-9.2Zm0 7.6a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm5.9-7.8a1.1 1.1 0 1 1-2.2 0 1.1 1.1 0 0 1 2.2 0ZM12 4.3c2.5 0 2.8 0 3.8.1 2.6.1 3.7 1.3 3.8 3.8.1 1 .1 1.3.1 3.8s0 2.8-.1 3.8c-.1 2.5-1.3 3.7-3.8 3.8-1 .1-1.3.1-3.8.1s-2.8 0-3.8-.1c-2.6-.1-3.7-1.3-3.8-3.8-.1-1-.1-1.3-.1-3.8s0-2.8.1-3.8c.1-2.5 1.3-3.7 3.8-3.8 1-.1 1.3-.1 3.8-.1Zm0-1.7c-2.6 0-2.9 0-3.9.1-3.5.2-5.4 2.1-5.6 5.6-.1 1-.1 1.3-.1 3.9s0 2.9.1 3.9c.2 3.5 2.1 5.4 5.6 5.6 1 .1 1.3.1 3.9.1s2.9 0 3.9-.1c3.5-.2 5.4-2.1 5.6-5.6.1-1 .1-1.3.1-3.9s0-2.9-.1-3.9c-.2-3.5-2.1-5.4-5.6-5.6-1-.1-1.3-.1-3.9-.1Z'],
|
|
10
|
+
[/(^|\.)(youtube\.com|youtu\.be)$/, 'M21.6 7.2a2.5 2.5 0 0 0-1.8-1.8C18.2 5 12 5 12 5s-6.2 0-7.8.4A2.5 2.5 0 0 0 2.4 7.2C2 8.8 2 12 2 12s0 3.2.4 4.8a2.5 2.5 0 0 0 1.8 1.8c1.6.4 7.8.4 7.8.4s6.2 0 7.8-.4a2.5 2.5 0 0 0 1.8-1.8c.4-1.6.4-4.8.4-4.8s0-3.2-.4-4.8ZM10 15V9l5.2 3L10 15Z'],
|
|
11
|
+
[/(^|\.)(facebook\.com|fb\.com)$/, 'M13.5 21v-7.6h2.6l.4-3h-3V8.5c0-.9.2-1.5 1.5-1.5h1.6V4.3c-.3 0-1.2-.1-2.3-.1-2.3 0-3.9 1.4-3.9 4v2.2H7.8v3h2.6V21h3.1Z'],
|
|
12
|
+
[/(^|\.)tiktok\.com$/, 'M16.6 3h-3.1v12.3a2.6 2.6 0 1 1-2.6-2.6c.3 0 .5 0 .8.1V9.6a5.8 5.8 0 1 0 4.9 5.7V9.1a7.3 7.3 0 0 0 4.3 1.4V7.4a4.3 4.3 0 0 1-4.3-4.4Z'],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const RSS = 'M5 3a16 16 0 0 1 16 16h-3A13 13 0 0 0 5 6V3Zm0 6a10 10 0 0 1 10 10h-3a7 7 0 0 0-7-7V9Zm1.5 6.5a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z';
|
|
16
|
+
const MAIL = 'M3 5h18a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm1.5 2v.3l7.5 5.2 7.5-5.2V7h-15Zm15 2.7-7.5 5.2-7.5-5.2V17h15V9.7Z';
|
|
17
|
+
|
|
18
|
+
// the SVG path for a social URL, or null when the host isn't recognised
|
|
19
|
+
export function socialIconPath(url: string): string | null {
|
|
20
|
+
if (url.startsWith('mailto:')) return MAIL;
|
|
21
|
+
if (/\/rss\.xml$|\/feed\/?$/.test(url)) return RSS;
|
|
22
|
+
let host = '';
|
|
23
|
+
try {
|
|
24
|
+
host = new URL(url).hostname.replace(/^www\./, '');
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return ICONS.find(([re]) => re.test(host))?.[1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const socialIconSvg = (path: string) =>
|
|
32
|
+
`<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="${path}"/></svg>`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Tag marker colors. Labels render as a small rounded-square marker + text
|
|
2
|
+
// (never a filled pill); each tag gets a stable hue hashed from its name, so
|
|
3
|
+
// the same tag is the same color on every page without any stored setting.
|
|
4
|
+
// Mid-tone hues chosen to read on both light and dark canvases.
|
|
5
|
+
const PALETTE = ['#3a86d6', '#2f9e6e', '#d9822b', '#8a6cf0', '#d45a8a', '#0c8599', '#c2872a', '#6b7fd7'];
|
|
6
|
+
|
|
7
|
+
export function tagColor(name: string): string {
|
|
8
|
+
let h = 0;
|
|
9
|
+
for (const c of name.toLowerCase()) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
|
10
|
+
return PALETTE[h % PALETTE.length];
|
|
11
|
+
}
|