@aglyn/aglyn 1.0.0-beta.206 → 1.0.0-beta.209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/announce-site-wide-change.d.ts +45 -0
  3. package/src/lib/app-utils/announce-site-wide-change.js +58 -0
  4. package/src/lib/app-utils/announce-site-wide-change.js.map +1 -0
  5. package/src/lib/app-utils/console-routes.d.ts +2 -0
  6. package/src/lib/app-utils/console-routes.js +3 -0
  7. package/src/lib/app-utils/console-routes.js.map +1 -1
  8. package/src/lib/app-utils/content-authors.d.ts +12 -64
  9. package/src/lib/app-utils/content-authors.js +0 -133
  10. package/src/lib/app-utils/content-authors.js.map +1 -1
  11. package/src/lib/app-utils/docs-help.generated.d.ts +6 -0
  12. package/src/lib/app-utils/docs-help.generated.js +61 -0
  13. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  14. package/src/lib/app-utils/docs-index.generated.js +4 -4
  15. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  16. package/src/lib/app-utils/health-report.js +13 -0
  17. package/src/lib/app-utils/health-report.js.map +1 -1
  18. package/src/lib/app-utils/host-transfer.d.ts +55 -0
  19. package/src/lib/app-utils/host-transfer.js +23 -0
  20. package/src/lib/app-utils/host-transfer.js.map +1 -0
  21. package/src/lib/app-utils/local-business.d.ts +161 -0
  22. package/src/lib/app-utils/local-business.js +253 -0
  23. package/src/lib/app-utils/local-business.js.map +1 -0
  24. package/src/lib/app-utils/notifications.d.ts +1 -13
  25. package/src/lib/app-utils/notifications.js +1 -16
  26. package/src/lib/app-utils/notifications.js.map +1 -1
  27. package/src/lib/app-utils/operator-alerts.d.ts +132 -0
  28. package/src/lib/app-utils/operator-alerts.js +513 -0
  29. package/src/lib/app-utils/operator-alerts.js.map +1 -0
  30. package/src/lib/app-utils/screen-link-value.d.ts +4 -0
  31. package/src/lib/app-utils/screen-link-value.js +6 -2
  32. package/src/lib/app-utils/screen-link-value.js.map +1 -1
  33. package/src/lib/app-utils/server.d.ts +1 -0
  34. package/src/lib/app-utils/server.js +9 -0
  35. package/src/lib/app-utils/server.js.map +1 -1
  36. package/src/lib/app-utils/site-entity-json-ld.d.ts +96 -0
  37. package/src/lib/app-utils/site-entity-json-ld.js +220 -0
  38. package/src/lib/app-utils/site-entity-json-ld.js.map +1 -0
  39. package/src/lib/foundation/definitions/platform.types.d.ts +24 -0
  40. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  41. package/src/lib/plugin-manager/feature-plugins.d.ts +9 -0
  42. package/src/lib/plugin-manager/feature-plugins.js +9 -0
  43. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  44. package/src/lib/plugin-manager/first-party-plugins.generated.js +8 -1
  45. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  46. package/src/lib/plugin-manager/operator-alerts.d.ts +37 -0
  47. package/src/lib/plugin-manager/operator-alerts.js +80 -0
  48. package/src/lib/plugin-manager/operator-alerts.js.map +1 -0
  49. package/src/lib/plugin-manager/plugin-host-collections.d.ts +11 -0
  50. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** Why a transfer cannot go ahead, in words a staffer can act on. */
18
+ export interface HostTransferHold {
19
+ code: 'no-site' | 'no-destination' | 'same-organization' | 'consent-group' | 'site-limit' | 'sending-domain' | 'in-flight';
20
+ message: string;
21
+ }
22
+ /** Everything a transfer would do, and everything it would not. */
23
+ export interface HostTransferPlan {
24
+ hostId: string;
25
+ siteName: string | null;
26
+ fromOrgId: string | null;
27
+ fromOrgName: string | null;
28
+ toOrgId: string;
29
+ toOrgName: string | null;
30
+ /** Refusals. The transfer runs only when this is empty. */
31
+ holds: HostTransferHold[];
32
+ /** What is true after the transfer that a staffer should say out loud. */
33
+ warnings: string[];
34
+ facts: {
35
+ /** Media in the old organization's library scoped to this site. */
36
+ mediaStaying: number;
37
+ /** Plugin documents the site owns that stay as the old organization's history. */
38
+ ownedDocumentsStaying: Array<{
39
+ collection: string;
40
+ count: number;
41
+ }>;
42
+ /** Old members whose only reach into the site was a per-site grant. */
43
+ collaboratorsLosingAccess: number;
44
+ /** The dedicated sending domain that moves with the site, if any. */
45
+ sendingDomain: string | null;
46
+ /** Plugins the site runs now that the destination does not enable. */
47
+ pluginsLost: string[];
48
+ /** The destination's sites against its limit. */
49
+ siteLimit: {
50
+ used: number;
51
+ limit: number;
52
+ allowed: boolean;
53
+ };
54
+ };
55
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /*
17
+ * What moving a site to another organization would do (AGL-3381), as the
18
+ * staff console's confirmation reads it. Planned and performed by
19
+ * `transferHost` in `@aglyn/tenant-data-admin/server/transfer-host`; the
20
+ * shape lives here so the dialog can read it without reaching a server module.
21
+ */ /** Why a transfer cannot go ahead, in words a staffer can act on. */ /** Everything a transfer would do, and everything it would not. */ export { };
22
+
23
+ //# sourceMappingURL=host-transfer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-transfer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * What moving a site to another organization would do (AGL-3381), as the\n * staff console's confirmation reads it. Planned and performed by\n * `transferHost` in `@aglyn/tenant-data-admin/server/transfer-host`; the\n * shape lives here so the dialog can read it without reaching a server module.\n */\n\n/** Why a transfer cannot go ahead, in words a staffer can act on. */\nexport interface HostTransferHold {\n code:\n | 'no-site'\n | 'no-destination'\n | 'same-organization'\n | 'consent-group'\n | 'site-limit'\n | 'sending-domain'\n | 'in-flight'\n message: string\n}\n\n/** Everything a transfer would do, and everything it would not. */\nexport interface HostTransferPlan {\n hostId: string\n siteName: string | null\n fromOrgId: string | null\n fromOrgName: string | null\n toOrgId: string\n toOrgName: string | null\n /** Refusals. The transfer runs only when this is empty. */\n holds: HostTransferHold[]\n /** What is true after the transfer that a staffer should say out loud. */\n warnings: string[]\n facts: {\n /** Media in the old organization's library scoped to this site. */\n mediaStaying: number\n /** Plugin documents the site owns that stay as the old organization's history. */\n ownedDocumentsStaying: Array<{ collection: string; count: number }>\n /** Old members whose only reach into the site was a per-site grant. */\n collaboratorsLosingAccess: number\n /** The dedicated sending domain that moves with the site, if any. */\n sendingDomain: string | null\n /** Plugins the site runs now that the destination does not enable. */\n pluginsLost: string[]\n /** The destination's sites against its limit. */\n siteLimit: { used: number; limit: number; allowed: boolean }\n }\n}\n\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GAED,mEAAmE,GAanE,iEAAiE,GACjE,WAyBC"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A site entity that is a LOCAL BUSINESS (AGL-3383).
19
+ *
20
+ * `host.seo.entity.type` answers Organization or Person and keeps answering
21
+ * it: it is a numeric enum stored as `"1"`/`"2"`, read by the author editor,
22
+ * the logo card and the AI audit, and a third value there would be a third
23
+ * value each of them has to learn. A plumber is still an Organization — the
24
+ * business type REFINES that answer rather than replacing it — so it is a
25
+ * field of its own, `businessType`, holding the `schema.org` subtype name.
26
+ *
27
+ * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the
28
+ * console's Select is built from too. A stored value outside it (a typo, a
29
+ * type removed from the list) publishes the plain Organization it would have
30
+ * published before the field existed, rather than an `@type` no consumer
31
+ * recognizes.
32
+ */
33
+ /** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */
34
+ export declare const LOCAL_BUSINESS_TYPES: readonly [{
35
+ readonly value: "LocalBusiness";
36
+ readonly label: "Local business (general)";
37
+ }, {
38
+ readonly value: "HomeAndConstructionBusiness";
39
+ readonly label: "Home and construction";
40
+ }, {
41
+ readonly value: "GeneralContractor";
42
+ readonly label: "General contractor";
43
+ }, {
44
+ readonly value: "HousePainter";
45
+ readonly label: "House painter";
46
+ }, {
47
+ readonly value: "Plumber";
48
+ readonly label: "Plumber";
49
+ }, {
50
+ readonly value: "Electrician";
51
+ readonly label: "Electrician";
52
+ }, {
53
+ readonly value: "HVACBusiness";
54
+ readonly label: "Heating and air (HVAC)";
55
+ }, {
56
+ readonly value: "Locksmith";
57
+ readonly label: "Locksmith";
58
+ }, {
59
+ readonly value: "RoofingContractor";
60
+ readonly label: "Roofing contractor";
61
+ }, {
62
+ readonly value: "MovingCompany";
63
+ readonly label: "Moving company";
64
+ }, {
65
+ readonly value: "ProfessionalService";
66
+ readonly label: "Professional service";
67
+ }, {
68
+ readonly value: "Store";
69
+ readonly label: "Store";
70
+ }, {
71
+ readonly value: "FoodEstablishment";
72
+ readonly label: "Food and drink";
73
+ }, {
74
+ readonly value: "Restaurant";
75
+ readonly label: "Restaurant";
76
+ }, {
77
+ readonly value: "HealthAndBeautyBusiness";
78
+ readonly label: "Health and beauty";
79
+ }, {
80
+ readonly value: "MedicalBusiness";
81
+ readonly label: "Medical practice";
82
+ }, {
83
+ readonly value: "Dentist";
84
+ readonly label: "Dentist";
85
+ }, {
86
+ readonly value: "AutomotiveBusiness";
87
+ readonly label: "Automotive";
88
+ }, {
89
+ readonly value: "LegalService";
90
+ readonly label: "Legal service";
91
+ }, {
92
+ readonly value: "AccountingService";
93
+ readonly label: "Accounting";
94
+ }, {
95
+ readonly value: "RealEstateAgent";
96
+ readonly label: "Real estate agent";
97
+ }, {
98
+ readonly value: "SportsActivityLocation";
99
+ readonly label: "Gym, sports or fitness";
100
+ }, {
101
+ readonly value: "ChildCare";
102
+ readonly label: "Child care";
103
+ }, {
104
+ readonly value: "AnimalShelter";
105
+ readonly label: "Animal shelter";
106
+ }];
107
+ export type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value'];
108
+ /** At most this many areas are published; a service area is a list, not a gazetteer. */
109
+ export declare const AREA_SERVED_MAX = 20;
110
+ export declare const AREA_SERVED_NAME_MAX_LENGTH = 120;
111
+ /** Seven days, twice over — room for split shifts without inviting a timetable. */
112
+ export declare const OPENING_HOURS_MAX_ROWS = 14;
113
+ /** Google reads a price range past 100 characters as not being one. */
114
+ export declare const PRICE_RANGE_MAX_LENGTH = 100;
115
+ export declare const PAYMENT_ACCEPTED_MAX_LENGTH = 200;
116
+ /**
117
+ * The stored business type, as the `schema.org` name it publishes under, or
118
+ * `undefined` when there is none or it is not on the list.
119
+ *
120
+ * Case-insensitive, returning the canonical spelling, because `@type` is
121
+ * case-sensitive to every consumer and `plumber` would name nothing.
122
+ */
123
+ export declare function localBusinessType(value: unknown): LocalBusinessType | undefined;
124
+ /**
125
+ * The areas a business serves: trimmed, blanks dropped, de-duplicated without
126
+ * regard to case, and capped.
127
+ *
128
+ * Accepts the stored array or a newline-separated string, so a value typed
129
+ * into the console's text box reads the same whichever shape reached the
130
+ * document.
131
+ */
132
+ export declare function normalizeAreaServed(value: unknown): string[];
133
+ declare const DAYS: readonly ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
134
+ export type OpeningHoursDay = (typeof DAYS)[number];
135
+ /** One line of opening hours: these days, from `opens` to `closes`. */
136
+ export interface OpeningHoursRow {
137
+ days: OpeningHoursDay[];
138
+ /** `HH:MM`, 24-hour. */
139
+ opens: string;
140
+ /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */
141
+ closes: string;
142
+ }
143
+ /**
144
+ * Opening hours, one line per set of days, in the syntax `schema.org`'s own
145
+ * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,
146
+ * `Mo,We,Fr 08:00-12:00`.
147
+ *
148
+ * A text box rather than a repeating group of pickers because it is what the
149
+ * settings forms can hold without a new control, and because it is the form
150
+ * a business already writes its hours in. A line that does not read — a
151
+ * mistyped day, `25:00` — is DROPPED rather than guessed at: a published
152
+ * opening time is a promise a customer drives across town on.
153
+ */
154
+ export declare function parseOpeningHours(value: unknown): OpeningHoursRow[];
155
+ /**
156
+ * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so
157
+ * the console can say which one before it is saved rather than after it
158
+ * silently fails to publish. Blank lines are not problems.
159
+ */
160
+ export declare function invalidOpeningHoursLines(value: unknown): number[];
161
+ export {};
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A site entity that is a LOCAL BUSINESS (AGL-3383).
18
+ *
19
+ * `host.seo.entity.type` answers Organization or Person and keeps answering
20
+ * it: it is a numeric enum stored as `"1"`/`"2"`, read by the author editor,
21
+ * the logo card and the AI audit, and a third value there would be a third
22
+ * value each of them has to learn. A plumber is still an Organization — the
23
+ * business type REFINES that answer rather than replacing it — so it is a
24
+ * field of its own, `businessType`, holding the `schema.org` subtype name.
25
+ *
26
+ * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the
27
+ * console's Select is built from too. A stored value outside it (a typo, a
28
+ * type removed from the list) publishes the plain Organization it would have
29
+ * published before the field existed, rather than an `@type` no consumer
30
+ * recognizes.
31
+ */ /** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */ export const LOCAL_BUSINESS_TYPES = [
32
+ {
33
+ value: 'LocalBusiness',
34
+ label: 'Local business (general)'
35
+ },
36
+ {
37
+ value: 'HomeAndConstructionBusiness',
38
+ label: 'Home and construction'
39
+ },
40
+ {
41
+ value: 'GeneralContractor',
42
+ label: 'General contractor'
43
+ },
44
+ {
45
+ value: 'HousePainter',
46
+ label: 'House painter'
47
+ },
48
+ {
49
+ value: 'Plumber',
50
+ label: 'Plumber'
51
+ },
52
+ {
53
+ value: 'Electrician',
54
+ label: 'Electrician'
55
+ },
56
+ {
57
+ value: 'HVACBusiness',
58
+ label: 'Heating and air (HVAC)'
59
+ },
60
+ {
61
+ value: 'Locksmith',
62
+ label: 'Locksmith'
63
+ },
64
+ {
65
+ value: 'RoofingContractor',
66
+ label: 'Roofing contractor'
67
+ },
68
+ {
69
+ value: 'MovingCompany',
70
+ label: 'Moving company'
71
+ },
72
+ {
73
+ value: 'ProfessionalService',
74
+ label: 'Professional service'
75
+ },
76
+ {
77
+ value: 'Store',
78
+ label: 'Store'
79
+ },
80
+ {
81
+ value: 'FoodEstablishment',
82
+ label: 'Food and drink'
83
+ },
84
+ {
85
+ value: 'Restaurant',
86
+ label: 'Restaurant'
87
+ },
88
+ {
89
+ value: 'HealthAndBeautyBusiness',
90
+ label: 'Health and beauty'
91
+ },
92
+ {
93
+ value: 'MedicalBusiness',
94
+ label: 'Medical practice'
95
+ },
96
+ {
97
+ value: 'Dentist',
98
+ label: 'Dentist'
99
+ },
100
+ {
101
+ value: 'AutomotiveBusiness',
102
+ label: 'Automotive'
103
+ },
104
+ {
105
+ value: 'LegalService',
106
+ label: 'Legal service'
107
+ },
108
+ {
109
+ value: 'AccountingService',
110
+ label: 'Accounting'
111
+ },
112
+ {
113
+ value: 'RealEstateAgent',
114
+ label: 'Real estate agent'
115
+ },
116
+ {
117
+ value: 'SportsActivityLocation',
118
+ label: 'Gym, sports or fitness'
119
+ },
120
+ {
121
+ value: 'ChildCare',
122
+ label: 'Child care'
123
+ },
124
+ {
125
+ value: 'AnimalShelter',
126
+ label: 'Animal shelter'
127
+ }
128
+ ];
129
+ /** At most this many areas are published; a service area is a list, not a gazetteer. */ export const AREA_SERVED_MAX = 20;
130
+ export const AREA_SERVED_NAME_MAX_LENGTH = 120;
131
+ /** Seven days, twice over — room for split shifts without inviting a timetable. */ export const OPENING_HOURS_MAX_ROWS = 14;
132
+ /** Google reads a price range past 100 characters as not being one. */ export const PRICE_RANGE_MAX_LENGTH = 100;
133
+ export const PAYMENT_ACCEPTED_MAX_LENGTH = 200;
134
+ /**
135
+ * The stored business type, as the `schema.org` name it publishes under, or
136
+ * `undefined` when there is none or it is not on the list.
137
+ *
138
+ * Case-insensitive, returning the canonical spelling, because `@type` is
139
+ * case-sensitive to every consumer and `plumber` would name nothing.
140
+ */ export function localBusinessType(value) {
141
+ var _LOCAL_BUSINESS_TYPES_find;
142
+ if (typeof value !== 'string') return undefined;
143
+ const wanted = value.trim().toLowerCase();
144
+ if (!wanted) return undefined;
145
+ return (_LOCAL_BUSINESS_TYPES_find = LOCAL_BUSINESS_TYPES.find((entry)=>entry.value.toLowerCase() === wanted)) == null ? void 0 : _LOCAL_BUSINESS_TYPES_find.value;
146
+ }
147
+ /**
148
+ * The areas a business serves: trimmed, blanks dropped, de-duplicated without
149
+ * regard to case, and capped.
150
+ *
151
+ * Accepts the stored array or a newline-separated string, so a value typed
152
+ * into the console's text box reads the same whichever shape reached the
153
+ * document.
154
+ */ export function normalizeAreaServed(value) {
155
+ const items = Array.isArray(value) ? value : typeof value === 'string' ? value.split(/\r?\n/) : [];
156
+ const seen = new Set();
157
+ const areas = [];
158
+ for (const item of items){
159
+ if (typeof item !== 'string') continue;
160
+ const name = item.trim().replace(/\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH);
161
+ const key = name.toLowerCase();
162
+ if (!name || seen.has(key)) continue;
163
+ seen.add(key);
164
+ areas.push(name);
165
+ if (areas.length >= AREA_SERVED_MAX) break;
166
+ }
167
+ return areas;
168
+ }
169
+ const DAYS = [
170
+ 'Monday',
171
+ 'Tuesday',
172
+ 'Wednesday',
173
+ 'Thursday',
174
+ 'Friday',
175
+ 'Saturday',
176
+ 'Sunday'
177
+ ];
178
+ /**
179
+ * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and
180
+ * `Monday` all work, which is every way a person abbreviates a weekday. A
181
+ * single letter is refused: `T` and `S` are each two days.
182
+ */ const dayIndex = (token)=>{
183
+ const wanted = token.trim().toLowerCase();
184
+ if (wanted.length < 2) return -1;
185
+ return DAYS.findIndex((day)=>day.toLowerCase().startsWith(wanted));
186
+ };
187
+ /** `9:00` → `09:00`; anything that is not a real time of day → `''`. */ const clockTime = (hours, minutes)=>{
188
+ const h = Number(hours);
189
+ const m = Number(minutes);
190
+ if (h > 23 || m > 59) return '';
191
+ return `${String(h).padStart(2, '0')}:${minutes}`;
192
+ };
193
+ const DASH = /\s*[-–—]\s*/;
194
+ const LINE = /^(.+?)\s+(\d{1,2}):(\d{2})\s*[-–—]\s*(\d{1,2}):(\d{2})$/;
195
+ /** One line, or `null` when any part of it does not read. */ function parseOpeningHoursLine(line) {
196
+ const match = LINE.exec(line.trim());
197
+ if (!match) return null;
198
+ const opens = clockTime(match[2], match[3]);
199
+ const closes = clockTime(match[4], match[5]);
200
+ if (!opens || !closes) return null;
201
+ const days = [];
202
+ for (const part of match[1].split(',')){
203
+ const ends = part.trim().split(DASH);
204
+ if (ends.length > 2) return null;
205
+ const from = dayIndex(ends[0]);
206
+ const to = dayIndex(ends[ends.length - 1]);
207
+ if (from < 0 || to < 0) return null;
208
+ // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.
209
+ for(let step = 0; step <= (to - from + 7) % 7; step++){
210
+ const day = DAYS[(from + step) % 7];
211
+ if (!days.includes(day)) days.push(day);
212
+ }
213
+ }
214
+ return days.length ? {
215
+ days,
216
+ opens,
217
+ closes
218
+ } : null;
219
+ }
220
+ const hoursLines = (value)=>typeof value === 'string' ? value.split(/\r?\n/) : [];
221
+ /**
222
+ * Opening hours, one line per set of days, in the syntax `schema.org`'s own
223
+ * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,
224
+ * `Mo,We,Fr 08:00-12:00`.
225
+ *
226
+ * A text box rather than a repeating group of pickers because it is what the
227
+ * settings forms can hold without a new control, and because it is the form
228
+ * a business already writes its hours in. A line that does not read — a
229
+ * mistyped day, `25:00` — is DROPPED rather than guessed at: a published
230
+ * opening time is a promise a customer drives across town on.
231
+ */ export function parseOpeningHours(value) {
232
+ const rows = [];
233
+ for (const line of hoursLines(value)){
234
+ if (!line.trim()) continue;
235
+ const row = parseOpeningHoursLine(line);
236
+ if (row) rows.push(row);
237
+ if (rows.length >= OPENING_HOURS_MAX_ROWS) break;
238
+ }
239
+ return rows;
240
+ }
241
+ /**
242
+ * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so
243
+ * the console can say which one before it is saved rather than after it
244
+ * silently fails to publish. Blank lines are not problems.
245
+ */ export function invalidOpeningHoursLines(value) {
246
+ const invalid = [];
247
+ hoursLines(value).forEach((line, index)=>{
248
+ if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1);
249
+ });
250
+ return invalid;
251
+ }
252
+
253
+ //# sourceMappingURL=local-business.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/local-business.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A site entity that is a LOCAL BUSINESS (AGL-3383).\n *\n * `host.seo.entity.type` answers Organization or Person and keeps answering\n * it: it is a numeric enum stored as `\"1\"`/`\"2\"`, read by the author editor,\n * the logo card and the AI audit, and a third value there would be a third\n * value each of them has to learn. A plumber is still an Organization — the\n * business type REFINES that answer rather than replacing it — so it is a\n * field of its own, `businessType`, holding the `schema.org` subtype name.\n *\n * Every value is checked against {@link LOCAL_BUSINESS_TYPES}, which the\n * console's Select is built from too. A stored value outside it (a typo, a\n * type removed from the list) publishes the plain Organization it would have\n * published before the field existed, rather than an `@type` no consumer\n * recognizes.\n */\n\n/** The `schema.org` LocalBusiness subtypes a site can declare, with their console labels. */\nexport const LOCAL_BUSINESS_TYPES = [\n { value: 'LocalBusiness', label: 'Local business (general)' },\n { value: 'HomeAndConstructionBusiness', label: 'Home and construction' },\n { value: 'GeneralContractor', label: 'General contractor' },\n { value: 'HousePainter', label: 'House painter' },\n { value: 'Plumber', label: 'Plumber' },\n { value: 'Electrician', label: 'Electrician' },\n { value: 'HVACBusiness', label: 'Heating and air (HVAC)' },\n { value: 'Locksmith', label: 'Locksmith' },\n { value: 'RoofingContractor', label: 'Roofing contractor' },\n { value: 'MovingCompany', label: 'Moving company' },\n { value: 'ProfessionalService', label: 'Professional service' },\n { value: 'Store', label: 'Store' },\n { value: 'FoodEstablishment', label: 'Food and drink' },\n { value: 'Restaurant', label: 'Restaurant' },\n { value: 'HealthAndBeautyBusiness', label: 'Health and beauty' },\n { value: 'MedicalBusiness', label: 'Medical practice' },\n { value: 'Dentist', label: 'Dentist' },\n { value: 'AutomotiveBusiness', label: 'Automotive' },\n { value: 'LegalService', label: 'Legal service' },\n { value: 'AccountingService', label: 'Accounting' },\n { value: 'RealEstateAgent', label: 'Real estate agent' },\n { value: 'SportsActivityLocation', label: 'Gym, sports or fitness' },\n { value: 'ChildCare', label: 'Child care' },\n { value: 'AnimalShelter', label: 'Animal shelter' },\n] as const\n\nexport type LocalBusinessType = (typeof LOCAL_BUSINESS_TYPES)[number]['value']\n\n/** At most this many areas are published; a service area is a list, not a gazetteer. */\nexport const AREA_SERVED_MAX = 20\nexport const AREA_SERVED_NAME_MAX_LENGTH = 120\n/** Seven days, twice over — room for split shifts without inviting a timetable. */\nexport const OPENING_HOURS_MAX_ROWS = 14\n/** Google reads a price range past 100 characters as not being one. */\nexport const PRICE_RANGE_MAX_LENGTH = 100\nexport const PAYMENT_ACCEPTED_MAX_LENGTH = 200\n\n/**\n * The stored business type, as the `schema.org` name it publishes under, or\n * `undefined` when there is none or it is not on the list.\n *\n * Case-insensitive, returning the canonical spelling, because `@type` is\n * case-sensitive to every consumer and `plumber` would name nothing.\n */\nexport function localBusinessType(value: unknown): LocalBusinessType | undefined {\n if (typeof value !== 'string') return undefined\n const wanted = value.trim().toLowerCase()\n if (!wanted) return undefined\n return LOCAL_BUSINESS_TYPES.find((entry) => entry.value.toLowerCase() === wanted)\n ?.value\n}\n\n/**\n * The areas a business serves: trimmed, blanks dropped, de-duplicated without\n * regard to case, and capped.\n *\n * Accepts the stored array or a newline-separated string, so a value typed\n * into the console's text box reads the same whichever shape reached the\n * document.\n */\nexport function normalizeAreaServed(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(/\\r?\\n/)\n : []\n const seen = new Set<string>()\n const areas: string[] = []\n for (const item of items) {\n if (typeof item !== 'string') continue\n const name = item.trim().replace(/\\s+/g, ' ').slice(0, AREA_SERVED_NAME_MAX_LENGTH)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n areas.push(name)\n if (areas.length >= AREA_SERVED_MAX) break\n }\n return areas\n}\n\nconst DAYS = [\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n 'Sunday',\n] as const\n\nexport type OpeningHoursDay = (typeof DAYS)[number]\n\n/** One line of opening hours: these days, from `opens` to `closes`. */\nexport interface OpeningHoursRow {\n days: OpeningHoursDay[]\n /** `HH:MM`, 24-hour. */\n opens: string\n /** `HH:MM`, 24-hour. Earlier than `opens` means the next morning. */\n closes: string\n}\n\n/**\n * A day token, by the shortest prefix that is unambiguous — `Mo`, `Mon` and\n * `Monday` all work, which is every way a person abbreviates a weekday. A\n * single letter is refused: `T` and `S` are each two days.\n */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return DAYS.findIndex((day) => day.toLowerCase().startsWith(wanted))\n}\n\n/** `9:00` → `09:00`; anything that is not a real time of day → `''`. */\nconst clockTime = (hours: string, minutes: string): string => {\n const h = Number(hours)\n const m = Number(minutes)\n if (h > 23 || m > 59) return ''\n return `${String(h).padStart(2, '0')}:${minutes}`\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** One line, or `null` when any part of it does not read. */\nfunction parseOpeningHoursLine(line: string): OpeningHoursRow | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const opens = clockTime(match[2], match[3])\n const closes = clockTime(match[4], match[5])\n if (!opens || !closes) return null\n const days: OpeningHoursDay[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n for (let step = 0; step <= (to - from + 7) % 7; step++) {\n const day = DAYS[(from + step) % 7]\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, opens, closes } : null\n}\n\nconst hoursLines = (value: unknown): string[] =>\n typeof value === 'string' ? value.split(/\\r?\\n/) : []\n\n/**\n * Opening hours, one line per set of days, in the syntax `schema.org`'s own\n * `openingHours` text uses: `Mo-Fr 09:00-17:00`, `Sa 10:00-14:00`,\n * `Mo,We,Fr 08:00-12:00`.\n *\n * A text box rather than a repeating group of pickers because it is what the\n * settings forms can hold without a new control, and because it is the form\n * a business already writes its hours in. A line that does not read — a\n * mistyped day, `25:00` — is DROPPED rather than guessed at: a published\n * opening time is a promise a customer drives across town on.\n */\nexport function parseOpeningHours(value: unknown): OpeningHoursRow[] {\n const rows: OpeningHoursRow[] = []\n for (const line of hoursLines(value)) {\n if (!line.trim()) continue\n const row = parseOpeningHoursLine(line)\n if (row) rows.push(row)\n if (rows.length >= OPENING_HOURS_MAX_ROWS) break\n }\n return rows\n}\n\n/**\n * The 1-based numbers of the lines {@link parseOpeningHours} would drop, so\n * the console can say which one before it is saved rather than after it\n * silently fails to publish. Blank lines are not problems.\n */\nexport function invalidOpeningHoursLines(value: unknown): number[] {\n const invalid: number[] = []\n hoursLines(value).forEach((line, index) => {\n if (line.trim() && !parseOpeningHoursLine(line)) invalid.push(index + 1)\n })\n return invalid\n}\n"],"names":["LOCAL_BUSINESS_TYPES","value","label","AREA_SERVED_MAX","AREA_SERVED_NAME_MAX_LENGTH","OPENING_HOURS_MAX_ROWS","PRICE_RANGE_MAX_LENGTH","PAYMENT_ACCEPTED_MAX_LENGTH","localBusinessType","undefined","wanted","trim","toLowerCase","find","entry","normalizeAreaServed","items","Array","isArray","split","seen","Set","areas","item","name","replace","slice","key","has","add","push","length","DAYS","dayIndex","token","findIndex","day","startsWith","clockTime","hours","minutes","h","Number","m","String","padStart","DASH","LINE","parseOpeningHoursLine","line","match","exec","opens","closes","days","part","ends","from","to","step","includes","hoursLines","parseOpeningHours","rows","row","invalidOpeningHoursLines","invalid","forEach","index"],"mappings":"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"}
@@ -22,7 +22,7 @@ import type { ITimestamp } from '@aglyn/shared-util-timestamp';
22
22
  * their owner. Types follow the common SaaS taxonomy so the console can
23
23
  * icon/group them without a registry.
24
24
  */
25
- export type AglynNotificationType = 'billing.invoice' | 'billing.paymentFailed' | 'billing.subscriptionCanceled' | 'billing.usage' | 'team.invite' | 'team.roleChanged' | 'team.hostAccessGranted' | 'content.formSubmission' | 'content.booking' | 'content.order' | 'content.lowStock' | 'content.taskAssigned' | 'content.taskReminder' | 'content.contactAssigned' | 'content.leadAssigned' | 'content.crmDailyDigest' | 'content.insightsDigest' | 'marketplace.review' | 'support.ticketOpened' | 'support.ticketReply' | 'system.announcement' | 'system.pluginVerifierRegression' | 'system.signInMethodRemoved' | 'system.scopeDrift' | 'system.ssoDomainUnverified' | 'system.formSubmissionsPaused' | 'system.visitorRecordsPaused' | 'system.abuseReportUrgent' | 'system.riskNotice' | 'system.dmcaCounterNotice' | 'system.bandwidthCeilingTripped' | 'system.bandwidthCapEngaged' | 'system.billingWebhookHalfApplied' | 'system.disputeUnattributed' | 'staff.userSignedUp' | 'staff.orgCreated' | 'staff.subscriptionStarted' | 'staff.subscriptionCanceled' | 'staff.planChanged' | 'staff.paymentFailed';
25
+ export type AglynNotificationType = 'billing.invoice' | 'billing.paymentFailed' | 'billing.subscriptionCanceled' | 'billing.usage' | 'team.invite' | 'team.roleChanged' | 'team.hostAccessGranted' | 'content.formSubmission' | 'content.booking' | 'content.order' | 'content.lowStock' | 'content.taskAssigned' | 'content.taskReminder' | 'content.contactAssigned' | 'content.leadAssigned' | 'content.crmDailyDigest' | 'content.insightsDigest' | 'marketplace.review' | 'support.ticketOpened' | 'support.ticketReply' | 'system.announcement' | 'system.pluginVerifierRegression' | 'system.signInMethodRemoved' | 'system.scopeDrift' | 'system.ssoDomainUnverified' | 'system.formSubmissionsPaused' | 'system.visitorRecordsPaused' | 'system.abuseReportUrgent' | 'system.riskNotice' | 'system.dmcaCounterNotice' | 'system.bandwidthCeilingTripped' | 'system.bandwidthCapEngaged' | 'system.billingWebhookHalfApplied' | 'system.disputeUnattributed' | 'system.operatorAlert' | 'staff.userSignedUp' | 'staff.orgCreated' | 'staff.subscriptionStarted' | 'staff.subscriptionCanceled' | 'staff.planChanged' | 'staff.paymentFailed';
26
26
  export interface AglynNotification {
27
27
  $id?: string;
28
28
  type: AglynNotificationType;
@@ -237,18 +237,6 @@ export declare const NOTIFICATION_CHANNEL_DEFAULTS: Record<NotificationCategory,
237
237
  * notification saying that the digest happened.
238
238
  */
239
239
  export declare const NOTIFICATION_SELF_SENT_EMAIL_TYPES: ReadonlySet<string>;
240
- /**
241
- * Staff notifications the OPERATOR must hear about by email, whoever runs the
242
- * install and whatever anyone switched on (AGL-3375): fraud and risk alerts,
243
- * a card dispute nobody owns, a DMCA counter-notice whose statutory clock is
244
- * running, and a billing webhook that applied only half of what it meant to.
245
- *
246
- * The console notification is still written for every staff member. What
247
- * this adds is one email to the operator's alert inbox, because the email
248
- * channel defaults OFF and a self-hosted or OSS install that nobody tuned
249
- * would otherwise learn about card testing on its checkout from its bank.
250
- */
251
- export declare const OPERATOR_ALERT_NOTIFICATION_TYPES: ReadonlySet<string>;
252
240
  /**
253
241
  * Whether a channel is on for one notification, at its own scope.
254
242
  *
@@ -49,6 +49,7 @@ export const NOTIFICATION_TYPE_LABELS = {
49
49
  'system.bandwidthCapEngaged': 'Monthly traffic limit reached',
50
50
  'system.billingWebhookHalfApplied': 'Billing webhook half applied',
51
51
  'system.disputeUnattributed': 'Card dispute with no owner',
52
+ 'system.operatorAlert': 'Operator alert',
52
53
  'staff.userSignedUp': 'New account',
53
54
  'staff.orgCreated': 'New workspace',
54
55
  'staff.subscriptionStarted': 'New subscription',
@@ -213,22 +214,6 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
213
214
  // Emailed by `notifyRiskEvent` itself, as account mail (AGL-3368).
214
215
  'system.riskNotice'
215
216
  ]);
216
- /**
217
- * Staff notifications the OPERATOR must hear about by email, whoever runs the
218
- * install and whatever anyone switched on (AGL-3375): fraud and risk alerts,
219
- * a card dispute nobody owns, a DMCA counter-notice whose statutory clock is
220
- * running, and a billing webhook that applied only half of what it meant to.
221
- *
222
- * The console notification is still written for every staff member. What
223
- * this adds is one email to the operator's alert inbox, because the email
224
- * channel defaults OFF and a self-hosted or OSS install that nobody tuned
225
- * would otherwise learn about card testing on its checkout from its bank.
226
- */ export const OPERATOR_ALERT_NOTIFICATION_TYPES = new Set([
227
- 'system.abuseReportUrgent',
228
- 'system.disputeUnattributed',
229
- 'system.dmcaCounterNotice',
230
- 'system.billingWebhookHalfApplied'
231
- ]);
232
217
  /**
233
218
  * Whether a channel is on for one notification, at its own scope.
234
219
  *