@aglyn/aglyn 1.0.0-beta.264 → 1.0.0-beta.266

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 (39) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +13 -0
  3. package/src/lib/app-utils/account-acquisition.js +84 -0
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/analytics-events.d.ts +70 -0
  6. package/src/lib/app-utils/analytics-events.js +8 -0
  7. package/src/lib/app-utils/analytics-events.js.map +1 -1
  8. package/src/lib/app-utils/docs-help-sections.generated.js +4 -1
  9. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
  11. package/src/lib/app-utils/docs-help.generated.js +8 -1
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +31 -10
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/expand-repeatables.js +14 -7
  16. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  17. package/src/lib/app-utils/health-report.d.ts +47 -7
  18. package/src/lib/app-utils/health-report.js +31 -3
  19. package/src/lib/app-utils/health-report.js.map +1 -1
  20. package/src/lib/app-utils/live-document-count.client.d.ts +28 -0
  21. package/src/lib/app-utils/live-document-count.client.js +35 -0
  22. package/src/lib/app-utils/live-document-count.client.js.map +1 -0
  23. package/src/lib/app-utils/live-document-count.d.ts +91 -0
  24. package/src/lib/app-utils/live-document-count.js +81 -0
  25. package/src/lib/app-utils/live-document-count.js.map +1 -0
  26. package/src/lib/app-utils/notification-sounds.d.ts +49 -0
  27. package/src/lib/app-utils/notification-sounds.js +54 -0
  28. package/src/lib/app-utils/notification-sounds.js.map +1 -0
  29. package/src/lib/app-utils/notifications.d.ts +2 -0
  30. package/src/lib/app-utils/notifications.js +32 -0
  31. package/src/lib/app-utils/notifications.js.map +1 -1
  32. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +10 -0
  33. package/src/lib/app-utils/screen-analytics-aggregate.js +11 -0
  34. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
  35. package/src/lib/plugin-manager/first-party-plugins.generated.js +6 -0
  36. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  37. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +14 -0
  38. package/src/lib/plugin-manager/plugin-activity-actions.js +30 -3
  39. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
@@ -0,0 +1,28 @@
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
+ import { type Query } from 'firebase/firestore';
18
+ /**
19
+ * The web SDK's half of the live-count rule (AGL-3918): how many of
20
+ * [documents] are not deleted, as `/api/hosts/resources` counts them for a
21
+ * plan allowance. Every document less the tombstones (`deletedAt != null`);
22
+ * an archived document is live and counted. See `live-document-count.ts` for
23
+ * why it is two aggregates.
24
+ *
25
+ * Rejects when either aggregate is refused, so a caller keeps whatever
26
+ * fallback it already has rather than reading a confident 0.
27
+ */
28
+ export declare function getLiveCountFromServer(documents: Query): Promise<number>;
@@ -0,0 +1,35 @@
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
+ */ import { getCountFromServer, query, where } from "firebase/firestore";
17
+ import { liveDocumentCount, TOMBSTONE_FIELD } from "./live-document-count.js";
18
+ /**
19
+ * The web SDK's half of the live-count rule (AGL-3918): how many of
20
+ * [documents] are not deleted, as `/api/hosts/resources` counts them for a
21
+ * plan allowance. Every document less the tombstones (`deletedAt != null`);
22
+ * an archived document is live and counted. See `live-document-count.ts` for
23
+ * why it is two aggregates.
24
+ *
25
+ * Rejects when either aggregate is refused, so a caller keeps whatever
26
+ * fallback it already has rather than reading a confident 0.
27
+ */ export async function getLiveCountFromServer(documents) {
28
+ const [total, tombstones] = await Promise.all([
29
+ getCountFromServer(documents),
30
+ getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null)))
31
+ ]);
32
+ return liveDocumentCount(total.data().count, tombstones.data().count);
33
+ }
34
+
35
+ //# sourceMappingURL=live-document-count.client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.client.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\nimport { getCountFromServer, query, where, type Query } from 'firebase/firestore'\nimport { liveDocumentCount, TOMBSTONE_FIELD } from './live-document-count'\n\n/**\n * The web SDK's half of the live-count rule (AGL-3918): how many of\n * [documents] are not deleted, as `/api/hosts/resources` counts them for a\n * plan allowance. Every document less the tombstones (`deletedAt != null`);\n * an archived document is live and counted. See `live-document-count.ts` for\n * why it is two aggregates.\n *\n * Rejects when either aggregate is refused, so a caller keeps whatever\n * fallback it already has rather than reading a confident 0.\n */\nexport async function getLiveCountFromServer(documents: Query): Promise<number> {\n const [total, tombstones] = await Promise.all([\n getCountFromServer(documents),\n getCountFromServer(query(documents, where(TOMBSTONE_FIELD, '!=', null))),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n"],"names":["getCountFromServer","query","where","liveDocumentCount","TOMBSTONE_FIELD","getLiveCountFromServer","documents","total","tombstones","Promise","all","data","count"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,EAAEC,KAAK,EAAEC,KAAK,QAAoB,qBAAoB;AACjF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,2BAAuB;AAE1E;;;;;;;;;CASC,GACD,OAAO,eAAeC,uBAAuBC,SAAgB;IAC3D,MAAM,CAACC,OAAOC,WAAW,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC5CV,mBAAmBM;QACnBN,mBAAmBC,MAAMK,WAAWJ,MAAME,iBAAiB,MAAM;KAClE;IACD,OAAOD,kBAAkBI,MAAMI,IAAI,GAAGC,KAAK,EAAEJ,WAAWG,IAAI,GAAGC,KAAK;AACtE"}
@@ -0,0 +1,91 @@
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
+ * How a plan allowance counts a collection whose delete is a SOFT delete
19
+ * (AGL-3918).
20
+ *
21
+ * The rule: a deleted document frees its slot, an archived one keeps it.
22
+ * Deleting stamps `deletedAt` and leaves the document in place (a tenant page
23
+ * keeps grafting until its next revalidate, a past order still names the
24
+ * product), so a plain `count()` charged a site for everything it had ever
25
+ * deleted: a product deleted on Starter never gave its slot back, and the
26
+ * Automation card read "4/25 workflows" beside a list of three. Archiving is a
27
+ * status the owner can reverse in place, so an archived product or a disabled
28
+ * workflow is still counted.
29
+ *
30
+ * ## Why total minus tombstones
31
+ *
32
+ * A live document has no `deletedAt` at all (created through
33
+ * `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
34
+ * writer, a marketplace install). Firestore cannot match an absent field, so
35
+ * `where('deletedAt', '==', null)` counts the second shape alone. The
36
+ * TOMBSTONES are one shape — a value that is not `null` — and
37
+ * `where('deletedAt', '!=', null)` matches exactly them, so the total less
38
+ * that count is exactly the documents the server calls live. Two aggregates,
39
+ * one billed read each at these sizes, on the automatic single-field index
40
+ * `deletedAt` already has; no composite index.
41
+ *
42
+ * This module is SDK-agnostic: it names the Admin SDK's query shape
43
+ * structurally so a plugin, core and the console reach it without a second
44
+ * copy of the rule. The web SDK's counterpart is
45
+ * `live-document-count.client.ts`.
46
+ */
47
+ /** The field every soft-deleting collection stamps on delete. */
48
+ export declare const TOMBSTONE_FIELD = "deletedAt";
49
+ /** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */
50
+ export declare function liveDocumentCount(total: number, tombstones: number): number;
51
+ /** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */
52
+ export declare function isLiveDocument(deletedAt: unknown): boolean;
53
+ /** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */
54
+ export interface LiveCountSnapshot {
55
+ data(): {
56
+ count: number;
57
+ };
58
+ }
59
+ /** An aggregate query the Admin SDK can execute directly. */
60
+ export interface LiveCountAggregate {
61
+ get(): Promise<LiveCountSnapshot>;
62
+ }
63
+ /** A collection or query the Admin SDK can count and filter. */
64
+ export interface LiveCountQuery<A extends LiveCountAggregate> {
65
+ count(): A;
66
+ where(field: string, op: '!=', value: null): {
67
+ count(): A;
68
+ };
69
+ }
70
+ /**
71
+ * The live documents [documents] matches: every one less the tombstones.
72
+ *
73
+ * `read` executes each aggregate, and defaults to executing it directly. A
74
+ * create that counts and writes in one transaction passes
75
+ * `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
76
+ * reads and the lock they take serializes a concurrent create (AGL-2231).
77
+ */
78
+ export declare function countLiveDocuments<A extends LiveCountAggregate>(documents: LiveCountQuery<A>, read?: (aggregate: A) => Promise<LiveCountSnapshot>): Promise<number>;
79
+ /** The `source.type` a platform-seeded starter template carries (AGL-687). */
80
+ export declare const STARTER_TEMPLATE_SOURCE = "starter";
81
+ /**
82
+ * Of a site's deleted templates, how many `templatesPerHost`'s first count
83
+ * included: `where('source.type', '!=', 'starter')` matches a template that
84
+ * carries a `source.type` other than a starter's, and skips one without the
85
+ * field. Subtracting these from that count leaves the live templates the
86
+ * allowance counts, without the composite index a second inequality on
87
+ * `deletedAt` beside it would need.
88
+ */
89
+ export declare function deletedTemplatesCounted(tombstones: ReadonlyArray<{
90
+ get(field: string): unknown;
91
+ }>): number;
@@ -0,0 +1,81 @@
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
+ * How a plan allowance counts a collection whose delete is a SOFT delete
18
+ * (AGL-3918).
19
+ *
20
+ * The rule: a deleted document frees its slot, an archived one keeps it.
21
+ * Deleting stamps `deletedAt` and leaves the document in place (a tenant page
22
+ * keeps grafting until its next revalidate, a past order still names the
23
+ * product), so a plain `count()` charged a site for everything it had ever
24
+ * deleted: a product deleted on Starter never gave its slot back, and the
25
+ * Automation card read "4/25 workflows" beside a list of three. Archiving is a
26
+ * status the owner can reverse in place, so an archived product or a disabled
27
+ * workflow is still counted.
28
+ *
29
+ * ## Why total minus tombstones
30
+ *
31
+ * A live document has no `deletedAt` at all (created through
32
+ * `/api/hosts/resources`) or an explicit `deletedAt: null` (every product
33
+ * writer, a marketplace install). Firestore cannot match an absent field, so
34
+ * `where('deletedAt', '==', null)` counts the second shape alone. The
35
+ * TOMBSTONES are one shape — a value that is not `null` — and
36
+ * `where('deletedAt', '!=', null)` matches exactly them, so the total less
37
+ * that count is exactly the documents the server calls live. Two aggregates,
38
+ * one billed read each at these sizes, on the automatic single-field index
39
+ * `deletedAt` already has; no composite index.
40
+ *
41
+ * This module is SDK-agnostic: it names the Admin SDK's query shape
42
+ * structurally so a plugin, core and the console reach it without a second
43
+ * copy of the rule. The web SDK's counterpart is
44
+ * `live-document-count.client.ts`.
45
+ */ /** The field every soft-deleting collection stamps on delete. */ export const TOMBSTONE_FIELD = 'deletedAt';
46
+ /** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */ export function liveDocumentCount(total, tombstones) {
47
+ return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0));
48
+ }
49
+ /** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */ export function isLiveDocument(deletedAt) {
50
+ return deletedAt == null;
51
+ }
52
+ /**
53
+ * The live documents [documents] matches: every one less the tombstones.
54
+ *
55
+ * `read` executes each aggregate, and defaults to executing it directly. A
56
+ * create that counts and writes in one transaction passes
57
+ * `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's
58
+ * reads and the lock they take serializes a concurrent create (AGL-2231).
59
+ */ export async function countLiveDocuments(documents, read = (aggregate)=>aggregate.get()) {
60
+ const [total, tombstones] = await Promise.all([
61
+ read(documents.count()),
62
+ read(documents.where(TOMBSTONE_FIELD, '!=', null).count())
63
+ ]);
64
+ return liveDocumentCount(total.data().count, tombstones.data().count);
65
+ }
66
+ /** The `source.type` a platform-seeded starter template carries (AGL-687). */ export const STARTER_TEMPLATE_SOURCE = 'starter';
67
+ /**
68
+ * Of a site's deleted templates, how many `templatesPerHost`'s first count
69
+ * included: `where('source.type', '!=', 'starter')` matches a template that
70
+ * carries a `source.type` other than a starter's, and skips one without the
71
+ * field. Subtracting these from that count leaves the live templates the
72
+ * allowance counts, without the composite index a second inequality on
73
+ * `deletedAt` beside it would need.
74
+ */ export function deletedTemplatesCounted(tombstones) {
75
+ return tombstones.filter((entry)=>{
76
+ const type = entry.get('source.type');
77
+ return type != null && type !== STARTER_TEMPLATE_SOURCE;
78
+ }).length;
79
+ }
80
+
81
+ //# sourceMappingURL=live-document-count.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/live-document-count.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 * How a plan allowance counts a collection whose delete is a SOFT delete\n * (AGL-3918).\n *\n * The rule: a deleted document frees its slot, an archived one keeps it.\n * Deleting stamps `deletedAt` and leaves the document in place (a tenant page\n * keeps grafting until its next revalidate, a past order still names the\n * product), so a plain `count()` charged a site for everything it had ever\n * deleted: a product deleted on Starter never gave its slot back, and the\n * Automation card read \"4/25 workflows\" beside a list of three. Archiving is a\n * status the owner can reverse in place, so an archived product or a disabled\n * workflow is still counted.\n *\n * ## Why total minus tombstones\n *\n * A live document has no `deletedAt` at all (created through\n * `/api/hosts/resources`) or an explicit `deletedAt: null` (every product\n * writer, a marketplace install). Firestore cannot match an absent field, so\n * `where('deletedAt', '==', null)` counts the second shape alone. The\n * TOMBSTONES are one shape — a value that is not `null` — and\n * `where('deletedAt', '!=', null)` matches exactly them, so the total less\n * that count is exactly the documents the server calls live. Two aggregates,\n * one billed read each at these sizes, on the automatic single-field index\n * `deletedAt` already has; no composite index.\n *\n * This module is SDK-agnostic: it names the Admin SDK's query shape\n * structurally so a plugin, core and the console reach it without a second\n * copy of the rule. The web SDK's counterpart is\n * `live-document-count.client.ts`.\n */\n\n/** The field every soft-deleting collection stamps on delete. */\nexport const TOMBSTONE_FIELD = 'deletedAt'\n\n/** The live count from the two aggregates, never below zero: they are separate reads, and a delete landing between them can make the second the larger. */\nexport function liveDocumentCount(total: number, tombstones: number): number {\n return Math.max(0, (Number(total) || 0) - (Number(tombstones) || 0))\n}\n\n/** Whether a stored document is live: no `deletedAt`, or `deletedAt: null`. */\nexport function isLiveDocument(deletedAt: unknown): boolean {\n return deletedAt == null\n}\n\n/** An aggregate query's snapshot, as the Admin SDK's `AggregateQuerySnapshot` answers it. */\nexport interface LiveCountSnapshot {\n data(): { count: number }\n}\n\n/** An aggregate query the Admin SDK can execute directly. */\nexport interface LiveCountAggregate {\n get(): Promise<LiveCountSnapshot>\n}\n\n/** A collection or query the Admin SDK can count and filter. */\nexport interface LiveCountQuery<A extends LiveCountAggregate> {\n count(): A\n where(field: string, op: '!=', value: null): { count(): A }\n}\n\n/**\n * The live documents [documents] matches: every one less the tombstones.\n *\n * `read` executes each aggregate, and defaults to executing it directly. A\n * create that counts and writes in one transaction passes\n * `(aggregate) => tx.get(aggregate)`, so both counts are the TRANSACTION's\n * reads and the lock they take serializes a concurrent create (AGL-2231).\n */\nexport async function countLiveDocuments<A extends LiveCountAggregate>(\n documents: LiveCountQuery<A>,\n read: (aggregate: A) => Promise<LiveCountSnapshot> = (aggregate) => aggregate.get(),\n): Promise<number> {\n const [total, tombstones] = await Promise.all([\n read(documents.count()),\n read(documents.where(TOMBSTONE_FIELD, '!=', null).count()),\n ])\n return liveDocumentCount(total.data().count, tombstones.data().count)\n}\n\n/** The `source.type` a platform-seeded starter template carries (AGL-687). */\nexport const STARTER_TEMPLATE_SOURCE = 'starter'\n\n/**\n * Of a site's deleted templates, how many `templatesPerHost`'s first count\n * included: `where('source.type', '!=', 'starter')` matches a template that\n * carries a `source.type` other than a starter's, and skips one without the\n * field. Subtracting these from that count leaves the live templates the\n * allowance counts, without the composite index a second inequality on\n * `deletedAt` beside it would need.\n */\nexport function deletedTemplatesCounted(\n tombstones: ReadonlyArray<{ get(field: string): unknown }>,\n): number {\n return tombstones.filter((entry) => {\n const type = entry.get('source.type')\n return type != null && type !== STARTER_TEMPLATE_SOURCE\n }).length\n}\n"],"names":["TOMBSTONE_FIELD","liveDocumentCount","total","tombstones","Math","max","Number","isLiveDocument","deletedAt","countLiveDocuments","documents","read","aggregate","get","Promise","all","count","where","data","STARTER_TEMPLATE_SOURCE","deletedTemplatesCounted","filter","entry","type","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GAED,+DAA+D,GAC/D,OAAO,MAAMA,kBAAkB,YAAW;AAE1C,yJAAyJ,GACzJ,OAAO,SAASC,kBAAkBC,KAAa,EAAEC,UAAkB;IACjE,OAAOC,KAAKC,GAAG,CAAC,GAAG,AAACC,CAAAA,OAAOJ,UAAU,CAAA,IAAMI,CAAAA,OAAOH,eAAe,CAAA;AACnE;AAEA,6EAA6E,GAC7E,OAAO,SAASI,eAAeC,SAAkB;IAC/C,OAAOA,aAAa;AACtB;AAkBA;;;;;;;CAOC,GACD,OAAO,eAAeC,mBACpBC,SAA4B,EAC5BC,OAAqD,CAACC,YAAcA,UAAUC,GAAG,EAAE;IAEnF,MAAM,CAACX,OAAOC,WAAW,GAAG,MAAMW,QAAQC,GAAG,CAAC;QAC5CJ,KAAKD,UAAUM,KAAK;QACpBL,KAAKD,UAAUO,KAAK,CAACjB,iBAAiB,MAAM,MAAMgB,KAAK;KACxD;IACD,OAAOf,kBAAkBC,MAAMgB,IAAI,GAAGF,KAAK,EAAEb,WAAWe,IAAI,GAAGF,KAAK;AACtE;AAEA,4EAA4E,GAC5E,OAAO,MAAMG,0BAA0B,UAAS;AAEhD;;;;;;;CAOC,GACD,OAAO,SAASC,wBACdjB,UAA0D;IAE1D,OAAOA,WAAWkB,MAAM,CAAC,CAACC;QACxB,MAAMC,OAAOD,MAAMT,GAAG,CAAC;QACvB,OAAOU,QAAQ,QAAQA,SAASJ;IAClC,GAAGK,MAAM;AACX"}
@@ -0,0 +1,49 @@
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
+ * The notification sounds a person picks from (Notification settings), the
19
+ * same on every surface: the console's chime, the APNs `sound` the Apple apps
20
+ * play and the Android notification channel.
21
+ *
22
+ * NO IMPORTS, on purpose: the native apps read this list through the
23
+ * generated contracts, so it must stay a plain module.
24
+ *
25
+ * The audio is synthesized by `tools/notification-sounds/generate.py`, which
26
+ * writes each sound to the three paths named here; the two lists name the
27
+ * same ids.
28
+ */
29
+ /** `users/{uid}.notificationSound`: the id of the sound the person picked. */
30
+ export declare const NOTIFICATION_SOUND_FIELD = "notificationSound";
31
+ export interface NotificationSound {
32
+ id: string;
33
+ label: string;
34
+ /** The console's file, served from `apps/console/public`. */
35
+ web: string;
36
+ /** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */
37
+ apple: string;
38
+ /**
39
+ * The Android raw resource and the notification channel that plays it. A
40
+ * channel's sound cannot change once created, so each sound has its own.
41
+ */
42
+ android: string;
43
+ }
44
+ /** In the order the picker lists them; the first is the default. */
45
+ export declare const NOTIFICATION_SOUNDS: readonly NotificationSound[];
46
+ /** What a person hears before they pick one. */
47
+ export declare const DEFAULT_NOTIFICATION_SOUND = "xylophone";
48
+ /** The stored pick as a sound, the default for no pick or an id no longer listed. */
49
+ export declare function notificationSound(value: unknown): NotificationSound;
@@ -0,0 +1,54 @@
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
+ * The notification sounds a person picks from (Notification settings), the
18
+ * same on every surface: the console's chime, the APNs `sound` the Apple apps
19
+ * play and the Android notification channel.
20
+ *
21
+ * NO IMPORTS, on purpose: the native apps read this list through the
22
+ * generated contracts, so it must stay a plain module.
23
+ *
24
+ * The audio is synthesized by `tools/notification-sounds/generate.py`, which
25
+ * writes each sound to the three paths named here; the two lists name the
26
+ * same ids.
27
+ */ /** `users/{uid}.notificationSound`: the id of the sound the person picked. */ export const NOTIFICATION_SOUND_FIELD = 'notificationSound';
28
+ function sound(id, label) {
29
+ return {
30
+ id,
31
+ label,
32
+ web: `/sounds/notifications/${id}.wav`,
33
+ apple: `aglyn-${id}.caf`,
34
+ android: `aglyn_sound_${id}`
35
+ };
36
+ }
37
+ /** In the order the picker lists them; the first is the default. */ export const NOTIFICATION_SOUNDS = [
38
+ sound('xylophone', 'Xylophone'),
39
+ sound('signature', 'Signature'),
40
+ sound('tada', 'Ta-da'),
41
+ sound('plink', 'Plink'),
42
+ sound('arcade', 'Arcade'),
43
+ sound('cowbell', 'Cowbell'),
44
+ sound('bubbles', 'Bubbles'),
45
+ sound('steel_pan', 'Steel pan'),
46
+ sound('cash_register', 'Cash register')
47
+ ];
48
+ /** What a person hears before they pick one. */ export const DEFAULT_NOTIFICATION_SOUND = 'xylophone';
49
+ /** The stored pick as a sound, the default for no pick or an id no longer listed. */ export function notificationSound(value) {
50
+ var _NOTIFICATION_SOUNDS_find;
51
+ return (_NOTIFICATION_SOUNDS_find = NOTIFICATION_SOUNDS.find((entry)=>entry.id === value)) != null ? _NOTIFICATION_SOUNDS_find : NOTIFICATION_SOUNDS.find((entry)=>entry.id === DEFAULT_NOTIFICATION_SOUND);
52
+ }
53
+
54
+ //# sourceMappingURL=notification-sounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/notification-sounds.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 * The notification sounds a person picks from (Notification settings), the\n * same on every surface: the console's chime, the APNs `sound` the Apple apps\n * play and the Android notification channel.\n *\n * NO IMPORTS, on purpose: the native apps read this list through the\n * generated contracts, so it must stay a plain module.\n *\n * The audio is synthesized by `tools/notification-sounds/generate.py`, which\n * writes each sound to the three paths named here; the two lists name the\n * same ids.\n */\n\n/** `users/{uid}.notificationSound`: the id of the sound the person picked. */\nexport const NOTIFICATION_SOUND_FIELD = 'notificationSound'\n\nexport interface NotificationSound {\n id: string\n label: string\n /** The console's file, served from `apps/console/public`. */\n web: string\n /** The file in both Apple apps' bundles, named in the APNs payload's `sound`. */\n apple: string\n /**\n * The Android raw resource and the notification channel that plays it. A\n * channel's sound cannot change once created, so each sound has its own.\n */\n android: string\n}\n\nfunction sound(id: string, label: string): NotificationSound {\n return {\n id,\n label,\n web: `/sounds/notifications/${id}.wav`,\n apple: `aglyn-${id}.caf`,\n android: `aglyn_sound_${id}`,\n }\n}\n\n/** In the order the picker lists them; the first is the default. */\nexport const NOTIFICATION_SOUNDS: readonly NotificationSound[] = [\n sound('xylophone', 'Xylophone'),\n sound('signature', 'Signature'),\n sound('tada', 'Ta-da'),\n sound('plink', 'Plink'),\n sound('arcade', 'Arcade'),\n sound('cowbell', 'Cowbell'),\n sound('bubbles', 'Bubbles'),\n sound('steel_pan', 'Steel pan'),\n sound('cash_register', 'Cash register'),\n]\n\n/** What a person hears before they pick one. */\nexport const DEFAULT_NOTIFICATION_SOUND = 'xylophone'\n\n/** The stored pick as a sound, the default for no pick or an id no longer listed. */\nexport function notificationSound(value: unknown): NotificationSound {\n return (\n NOTIFICATION_SOUNDS.find((entry) => entry.id === value) ??\n (NOTIFICATION_SOUNDS.find((entry) => entry.id === DEFAULT_NOTIFICATION_SOUND) as NotificationSound)\n )\n}\n"],"names":["NOTIFICATION_SOUND_FIELD","sound","id","label","web","apple","android","NOTIFICATION_SOUNDS","DEFAULT_NOTIFICATION_SOUND","notificationSound","value","find","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,4EAA4E,GAC5E,OAAO,MAAMA,2BAA2B,oBAAmB;AAgB3D,SAASC,MAAMC,EAAU,EAAEC,KAAa;IACtC,OAAO;QACLD;QACAC;QACAC,KAAK,CAAC,sBAAsB,EAAEF,GAAG,IAAI,CAAC;QACtCG,OAAO,CAAC,MAAM,EAAEH,GAAG,IAAI,CAAC;QACxBI,SAAS,CAAC,YAAY,EAAEJ,IAAI;IAC9B;AACF;AAEA,kEAAkE,GAClE,OAAO,MAAMK,sBAAoD;IAC/DN,MAAM,aAAa;IACnBA,MAAM,aAAa;IACnBA,MAAM,QAAQ;IACdA,MAAM,SAAS;IACfA,MAAM,UAAU;IAChBA,MAAM,WAAW;IACjBA,MAAM,WAAW;IACjBA,MAAM,aAAa;IACnBA,MAAM,iBAAiB;CACxB,CAAA;AAED,8CAA8C,GAC9C,OAAO,MAAMO,6BAA6B,YAAW;AAErD,mFAAmF,GACnF,OAAO,SAASC,kBAAkBC,KAAc;QAE5CH;IADF,QACEA,4BAAAA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKQ,kBAAjDH,4BACCA,oBAAoBI,IAAI,CAAC,CAACC,QAAUA,MAAMV,EAAE,KAAKM;AAEtD"}
@@ -349,6 +349,8 @@ export declare const NOTIFICATION_CHANNEL_DEFAULTS: Readonly<Record<Notification
349
349
  * see {@link NOTIFICATION_SELF_SENT_EMAIL_TYPES} and the `skipEmail` callers.
350
350
  */
351
351
  export declare const NOTIFICATION_TYPE_CHANNEL_DEFAULTS: Partial<Record<AglynNotificationType, Partial<Record<NotificationChannel, boolean>>>>;
352
+ /** The words on a notification email's button: the record it opens, else "Open in {product}". */
353
+ export declare function notificationEmailAction(type: AglynNotificationType | string, productName: string): string;
352
354
  /** What a type does on a channel when nobody has answered for it or its category. */
353
355
  export declare function notificationTypeChannelDefault(type: AglynNotificationType | string, channel: NotificationChannel): boolean;
354
356
  /**
@@ -443,6 +443,38 @@ export const NOTIFICATION_CHANNEL_DEFAULTS = inCategoryOrder(CORE_CHANNEL_DEFAUL
443
443
  email: true
444
444
  }
445
445
  };
446
+ /**
447
+ * What a notification email's button says it opens (AGL-3897), by type: the
448
+ * record the notification links to, named, rather than "Open in Aglyn" on
449
+ * every one. A type with no entry keeps that general label.
450
+ */ const NOTIFICATION_EMAIL_ACTIONS = {
451
+ 'content.formSubmission': 'View submission',
452
+ 'content.booking': 'View booking',
453
+ 'content.order': 'View order',
454
+ 'content.lowStock': 'View product',
455
+ 'content.taskAssigned': 'Open task',
456
+ 'content.taskReminder': 'Open task',
457
+ 'content.contactAssigned': 'Open contact',
458
+ 'content.leadAssigned': 'Open lead',
459
+ 'billing.invoice': 'Open billing',
460
+ 'billing.paymentFailed': 'Update payment method',
461
+ 'billing.subscriptionCanceled': 'Open billing',
462
+ 'billing.usage': 'Open billing',
463
+ 'support.ticketOpened': 'Open ticket',
464
+ 'support.ticketReply': 'Open ticket',
465
+ 'marketplace.review': 'Open listing',
466
+ 'staff.userSignedUp': 'Open account',
467
+ 'staff.orgCreated': 'Open workspace',
468
+ 'staff.siteCreated': 'Open site',
469
+ 'staff.subscriptionStarted': 'Open workspace',
470
+ 'staff.subscriptionCanceled': 'Open workspace',
471
+ 'staff.planChanged': 'Open workspace',
472
+ 'staff.paymentFailed': 'Open workspace'
473
+ };
474
+ /** The words on a notification email's button: the record it opens, else "Open in {product}". */ export function notificationEmailAction(type, productName) {
475
+ var _NOTIFICATION_EMAIL_ACTIONS_type;
476
+ return (_NOTIFICATION_EMAIL_ACTIONS_type = NOTIFICATION_EMAIL_ACTIONS[type]) != null ? _NOTIFICATION_EMAIL_ACTIONS_type : `Open in ${productName}`;
477
+ }
446
478
  /** What a type does on a channel when nobody has answered for it or its category. */ export function notificationTypeChannelDefault(type, channel) {
447
479
  var _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type;
448
480
  const own = (_NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type = NOTIFICATION_TYPE_CHANNEL_DEFAULTS[type]) == null ? void 0 : _NOTIFICATION_TYPE_CHANNEL_DEFAULTS_type[channel];