@aglyn/aglyn 1.0.0-beta.270 → 1.0.0-beta.271
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/package.json +11 -11
- package/src/lib/app-utils/activity-labels.js +2 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +13 -0
- package/src/lib/app-utils/analytics-events.js +2 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/append-only-history.d.ts +94 -0
- package/src/lib/app-utils/append-only-history.js +73 -0
- package/src/lib/app-utils/append-only-history.js.map +1 -0
- package/src/lib/app-utils/bundle-intent.d.ts +186 -0
- package/src/lib/app-utils/bundle-intent.js +386 -0
- package/src/lib/app-utils/bundle-intent.js.map +1 -0
- package/src/lib/app-utils/client-build.d.ts +204 -0
- package/src/lib/app-utils/client-build.js +237 -0
- package/src/lib/app-utils/client-build.js.map +1 -0
- package/src/lib/app-utils/console-routes.d.ts +4 -0
- package/src/lib/app-utils/console-routes.js +2 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +5 -5
- package/src/lib/app-utils/docs-help.generated.js +9 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +101 -31
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.js +6 -4
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/media-filter.d.ts +26 -0
- package/src/lib/app-utils/media-filter.js +32 -0
- package/src/lib/app-utils/media-filter.js.map +1 -1
- package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
- package/src/lib/app-utils/onboarding-deep-link.js +48 -5
- package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +21 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
- package/src/lib/app-utils/owner-approval-record.js +148 -0
- package/src/lib/app-utils/owner-approval-record.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +183 -67
- package/src/lib/app-utils/plan-entitlements.js +213 -58
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +1 -1
- package/src/lib/app-utils/release-flags.js +10 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +4 -0
- package/src/lib/app-utils/server.js +7 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-care-project.d.ts +227 -0
- package/src/lib/app-utils/site-care-project.js +423 -0
- package/src/lib/app-utils/site-care-project.js.map +1 -0
- package/src/lib/app-utils/site-care.d.ts +268 -0
- package/src/lib/app-utils/site-care.js +283 -0
- package/src/lib/app-utils/site-care.js.map +1 -0
- package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
- package/src/lib/app-utils/upgrade-proposal.js +60 -9
- package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +40 -10
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +48 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
- package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
- package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
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 STAFF BUILD FOR A CLIENT, ON ONE SCREEN (AGL-3958).
|
|
18
|
+
*
|
|
19
|
+
* At a booth or on a doorstep there is no laptop: staff create the client's
|
|
20
|
+
* workspace with "Build for a client" on (AGL-3952), and land on one screen
|
|
21
|
+
* that asks the business details once, records the domain the owner wants,
|
|
22
|
+
* shows the photo link's QR (AGL-3957), starts the guided AI start with those
|
|
23
|
+
* answers, and sends the owner the preview. This module is that screen's
|
|
24
|
+
* vocabulary: what is asked, what an answer may be, and the intent the
|
|
25
|
+
* hand-off carries into the owner's checkout.
|
|
26
|
+
*
|
|
27
|
+
* Pure: shapes, bounds and parsers, read by the screen, the staff route that
|
|
28
|
+
* stores the record and the hand-off that reads its intent. No React, no
|
|
29
|
+
* network.
|
|
30
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
31
|
+
import { normalizePhone } from "../foundation/definitions/contact.types.js";
|
|
32
|
+
import { SELF_SERVE_PLANS } from "./plan-entitlements.js";
|
|
33
|
+
/** The longest each answer may be, in characters. */ export const CLIENT_BUILD_LIMITS = {
|
|
34
|
+
// The three the guided start passes on as its own inputs, which it bounds
|
|
35
|
+
// at 120 (`AI_SITE_INPUT_MAX_CHARS`).
|
|
36
|
+
businessName: 120,
|
|
37
|
+
whatTheyDo: 120,
|
|
38
|
+
towns: 120,
|
|
39
|
+
phone: 32,
|
|
40
|
+
email: 254,
|
|
41
|
+
hours: 200,
|
|
42
|
+
ownerWords: 1500
|
|
43
|
+
};
|
|
44
|
+
/** An intake with nothing typed: what the screen starts on. */ export const EMPTY_CLIENT_BUILD_INTAKE = {
|
|
45
|
+
businessName: '',
|
|
46
|
+
whatTheyDo: '',
|
|
47
|
+
towns: '',
|
|
48
|
+
phone: '',
|
|
49
|
+
email: '',
|
|
50
|
+
hours: '',
|
|
51
|
+
ownerWords: ''
|
|
52
|
+
};
|
|
53
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
54
|
+
/**
|
|
55
|
+
* Why each answer cannot be used, in a sentence shown beside it; an answer
|
|
56
|
+
* that is fine has no entry. The name and what they do are required, since
|
|
57
|
+
* the site cannot be named or planned without them; the phone and the email
|
|
58
|
+
* are each optional, but the preview needs one of them to reach the owner.
|
|
59
|
+
*/ export function clientBuildIntakeErrors(intake) {
|
|
60
|
+
var _errors, _businessName, _errors1, _whatTheyDo, _errors2, _phone, _errors3, _email, _errors4, _phone1;
|
|
61
|
+
const errors = {};
|
|
62
|
+
for (const field of Object.keys(CLIENT_BUILD_LIMITS)){
|
|
63
|
+
if (intake[field].trim().length > CLIENT_BUILD_LIMITS[field]) {
|
|
64
|
+
errors[field] = `Keep this under ${CLIENT_BUILD_LIMITS[field].toLocaleString('en-US')} characters.`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!intake.businessName.trim()) (_businessName = (_errors = errors).businessName) != null ? _businessName : _errors.businessName = 'Name the business.';
|
|
68
|
+
if (!intake.whatTheyDo.trim()) (_whatTheyDo = (_errors1 = errors).whatTheyDo) != null ? _whatTheyDo : _errors1.whatTheyDo = 'Say what the business does.';
|
|
69
|
+
const phone = intake.phone.trim();
|
|
70
|
+
const email = intake.email.trim();
|
|
71
|
+
if (phone && !normalizePhone(phone)) (_phone = (_errors2 = errors).phone) != null ? _phone : _errors2.phone = 'Type a phone number with its area code.';
|
|
72
|
+
if (email && !EMAIL_PATTERN.test(email)) (_email = (_errors3 = errors).email) != null ? _email : _errors3.email = 'Type an email address like name@example.com.';
|
|
73
|
+
if (!phone && !email) (_phone1 = (_errors4 = errors).phone) != null ? _phone1 : _errors4.phone = 'Add a phone or an email, so the owner gets the preview.';
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* An intake as the route admits it: every answer a trimmed string within its
|
|
78
|
+
* bound, or the first reason it is not. Fields the body does not carry are
|
|
79
|
+
* empty, never `undefined`, so the record always has the same shape.
|
|
80
|
+
*/ export function parseClientBuildIntake(raw) {
|
|
81
|
+
if (!raw || typeof raw !== 'object') return 'Missing the business details';
|
|
82
|
+
const source = raw;
|
|
83
|
+
const intake = _extends({}, EMPTY_CLIENT_BUILD_INTAKE);
|
|
84
|
+
for (const field of Object.keys(CLIENT_BUILD_LIMITS)){
|
|
85
|
+
const value = source[field];
|
|
86
|
+
if (value === undefined || value === null) continue;
|
|
87
|
+
if (typeof value !== 'string') return `${field} must be text`;
|
|
88
|
+
intake[field] = value.trim();
|
|
89
|
+
}
|
|
90
|
+
const errors = clientBuildIntakeErrors(intake);
|
|
91
|
+
const first = Object.values(errors)[0];
|
|
92
|
+
return first != null ? first : intake;
|
|
93
|
+
}
|
|
94
|
+
/** The owner's phone as a text goes to it, E.164, or `null` when there is none to text. */ export function clientBuildPhone(intake) {
|
|
95
|
+
return normalizePhone(intake.phone);
|
|
96
|
+
}
|
|
97
|
+
/** A registrable name: labels of letters, digits and hyphens, and a TLD of letters. */ const DOMAIN_PATTERN = /^(?=.{3,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
98
|
+
/**
|
|
99
|
+
* A domain name as typed, normalized — lower case, no scheme, no `www.`, no
|
|
100
|
+
* path — or `null` when it is not one. "I have example.com" is typed on a
|
|
101
|
+
* phone, often as the address bar showed it.
|
|
102
|
+
*/ export function normalizeClientBuildDomainName(raw) {
|
|
103
|
+
if (typeof raw !== 'string') return null;
|
|
104
|
+
const name = raw.trim().toLowerCase().replace(/^[a-z]+:\/\//, '').replace(/^www\./, '').replace(/[/?#].*$/, '').replace(/\.$/, '');
|
|
105
|
+
return DOMAIN_PATTERN.test(name) ? name : null;
|
|
106
|
+
}
|
|
107
|
+
const isPrice = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0 && value < 1000000;
|
|
108
|
+
/** A Domain step answer as the route admits it, or the reason it is not one. */ export function parseClientBuildDomain(raw) {
|
|
109
|
+
if (!raw || typeof raw !== 'object') return 'Missing the domain choice';
|
|
110
|
+
const source = raw;
|
|
111
|
+
if (source['choice'] === 'free') return {
|
|
112
|
+
choice: 'free'
|
|
113
|
+
};
|
|
114
|
+
const name = normalizeClientBuildDomainName(source['name']);
|
|
115
|
+
if (source['choice'] === 'own') {
|
|
116
|
+
return name ? {
|
|
117
|
+
choice: 'own',
|
|
118
|
+
name
|
|
119
|
+
} : 'Type the domain the owner has, like example.com';
|
|
120
|
+
}
|
|
121
|
+
if (source['choice'] === 'register') {
|
|
122
|
+
if (!name) return 'Pick a domain from the search';
|
|
123
|
+
if (!isPrice(source['firstYearUsd']) || !isPrice(source['renewalUsd'])) {
|
|
124
|
+
return 'The domain needs its first-year and renewal prices';
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
choice: 'register',
|
|
128
|
+
name,
|
|
129
|
+
firstYearUsd: source['firstYearUsd'],
|
|
130
|
+
renewalUsd: source['renewalUsd']
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return 'Unknown domain choice';
|
|
134
|
+
}
|
|
135
|
+
/** "$12.99" for a whole-dollar-and-cents price. */ export function formatDomainUsd(amount) {
|
|
136
|
+
return `$${amount.toFixed(2)}`;
|
|
137
|
+
}
|
|
138
|
+
/** "$12.99 the first year, then $18.99 a year": both prices together, always. */ export function domainPriceText(prices) {
|
|
139
|
+
return prices.firstYearUsd === prices.renewalUsd ? `${formatDomainUsd(prices.firstYearUsd)} a year` : `${formatDomainUsd(prices.firstYearUsd)} the first year, then ${formatDomainUsd(prices.renewalUsd)} a year`;
|
|
140
|
+
}
|
|
141
|
+
/** Site Care rides every build for a client unless staff take it off (AGL-3954). */ export const CLIENT_BUILD_DEFAULT_ADDONS = [
|
|
142
|
+
'siteCare'
|
|
143
|
+
];
|
|
144
|
+
/** The plans a build may propose: every self-serve tier but Free. */ const INTENT_PLANS = SELF_SERVE_PLANS.filter((plan)=>plan !== 'free');
|
|
145
|
+
/** The intent a build starts with: the plan the workspace was comped on, Site Care, monthly. */ export function clientBuildIntent(options) {
|
|
146
|
+
const plan = options.plan && INTENT_PLANS.includes(options.plan) ? options.plan : 'starter';
|
|
147
|
+
const domain = options.domain;
|
|
148
|
+
return _extends({
|
|
149
|
+
plan,
|
|
150
|
+
addons: [
|
|
151
|
+
...CLIENT_BUILD_DEFAULT_ADDONS
|
|
152
|
+
],
|
|
153
|
+
interval: 'month'
|
|
154
|
+
}, (domain == null ? void 0 : domain.choice) === 'register' ? {
|
|
155
|
+
domain: {
|
|
156
|
+
name: domain.name,
|
|
157
|
+
firstYearUsd: domain.firstYearUsd,
|
|
158
|
+
renewalUsd: domain.renewalUsd
|
|
159
|
+
}
|
|
160
|
+
} : {}, (domain == null ? void 0 : domain.choice) === 'own' ? {
|
|
161
|
+
existingDomain: domain.name
|
|
162
|
+
} : {});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A stored intent, narrowed, or `null` when it is not one worth acting on.
|
|
166
|
+
* What the hand-off reads: a plan this build cannot sell, an add-on it does
|
|
167
|
+
* not know or a malformed domain reads as absent rather than as a checkout
|
|
168
|
+
* for something that does not exist.
|
|
169
|
+
*/ export function readClientBuildIntent(raw) {
|
|
170
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
171
|
+
const source = raw;
|
|
172
|
+
const plan = source['plan'];
|
|
173
|
+
if (typeof plan !== 'string' || !INTENT_PLANS.includes(plan)) return null;
|
|
174
|
+
const addons = Array.isArray(source['addons']) ? source['addons'].filter((addon)=>typeof addon === 'string' && addon.length > 0) : [];
|
|
175
|
+
const interval = source['interval'] === 'year' ? 'year' : 'month';
|
|
176
|
+
const coupon = typeof source['coupon'] === 'string' && source['coupon'].trim() ? source['coupon'].trim() : undefined;
|
|
177
|
+
const rawDomain = source['domain'];
|
|
178
|
+
const parsed = rawDomain && typeof rawDomain === 'object' ? parseClientBuildDomain(_extends({}, rawDomain, {
|
|
179
|
+
choice: 'register'
|
|
180
|
+
})) : null;
|
|
181
|
+
const domain = parsed && typeof parsed === 'object' && parsed.choice === 'register' ? parsed : null;
|
|
182
|
+
const existing = normalizeClientBuildDomainName(source['existingDomain']);
|
|
183
|
+
return _extends({
|
|
184
|
+
plan: plan,
|
|
185
|
+
addons,
|
|
186
|
+
interval
|
|
187
|
+
}, coupon ? {
|
|
188
|
+
coupon
|
|
189
|
+
} : {}, domain ? {
|
|
190
|
+
domain: {
|
|
191
|
+
name: domain.name,
|
|
192
|
+
firstYearUsd: domain.firstYearUsd,
|
|
193
|
+
renewalUsd: domain.renewalUsd
|
|
194
|
+
}
|
|
195
|
+
} : existing ? {
|
|
196
|
+
existingDomain: existing
|
|
197
|
+
} : {});
|
|
198
|
+
}
|
|
199
|
+
/** The subcollection and document the record lives at under its org. */ export const CLIENT_BUILD_COLLECTION = 'clientBuild';
|
|
200
|
+
export const CLIENT_BUILD_DOC_ID = 'current';
|
|
201
|
+
/** The screen's steps, in order. */ export const CLIENT_BUILD_STEPS = [
|
|
202
|
+
'details',
|
|
203
|
+
'domain',
|
|
204
|
+
'photos',
|
|
205
|
+
'build',
|
|
206
|
+
'send'
|
|
207
|
+
];
|
|
208
|
+
/**
|
|
209
|
+
* Where a stored build resumes: the first step it has not finished. A phone
|
|
210
|
+
* that reloads mid-build, or a second phone, opens where the first left off.
|
|
211
|
+
*/ export function clientBuildResumeStep(record) {
|
|
212
|
+
if (!record) return 'details';
|
|
213
|
+
if (!record.domain) return 'domain';
|
|
214
|
+
// The site exists before its photos are asked for, and the guided start
|
|
215
|
+
// waits on them: until a job is started, or the starter site was given
|
|
216
|
+
// instead, the build is at the photos.
|
|
217
|
+
if (!record.hostId || !record.jobId && !record.starter) return 'photos';
|
|
218
|
+
return 'send';
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The address the site is going to live at, as the owner is told it: the
|
|
222
|
+
* domain they picked or already have, else the free address it is on.
|
|
223
|
+
*/ export function clientBuildSiteAddress(domain, liveUrl) {
|
|
224
|
+
if (domain && domain.choice !== 'free') return domain.name;
|
|
225
|
+
return liveUrl ? liveUrl.replace(/^https?:\/\//, '') : null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The text the owner gets with their preview: short enough for one or two
|
|
229
|
+
* segments, and naming who it is from, since it arrives from a number they
|
|
230
|
+
* do not know. The email says the same at length, from the catalog.
|
|
231
|
+
*/ export function clientBuildPreviewText(options) {
|
|
232
|
+
const name = options.businessName.trim() || 'your business';
|
|
233
|
+
const address = options.siteAddress && options.siteAddress !== options.previewUrl.replace(/^https?:\/\//, '') ? ` It will live at ${options.siteAddress}.` : '';
|
|
234
|
+
return `${options.productName}: the new website for ${name} is ready to look at: ${options.previewUrl}${address}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//# sourceMappingURL=client-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/client-build.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 STAFF BUILD FOR A CLIENT, ON ONE SCREEN (AGL-3958).\n *\n * At a booth or on a doorstep there is no laptop: staff create the client's\n * workspace with \"Build for a client\" on (AGL-3952), and land on one screen\n * that asks the business details once, records the domain the owner wants,\n * shows the photo link's QR (AGL-3957), starts the guided AI start with those\n * answers, and sends the owner the preview. This module is that screen's\n * vocabulary: what is asked, what an answer may be, and the intent the\n * hand-off carries into the owner's checkout.\n *\n * Pure: shapes, bounds and parsers, read by the screen, the staff route that\n * stores the record and the hand-off that reads its intent. No React, no\n * network.\n */\n\nimport type { OrgPlan, OrgSeatAddons } from '../foundation/definitions/org-billing.types'\nimport { normalizePhone } from '../foundation/definitions/contact.types'\nimport { SELF_SERVE_PLANS } from './plan-entitlements'\n\n/** What staff ask the owner, once, and the guided start reads instead of asking again. */\nexport interface ClientBuildIntake {\n /** The business's name, as its header and footer will say it. */\n businessName: string\n /** What the business does, in a few words: the guided start's first answer. */\n whatTheyDo: string\n /** The towns or area it serves; empty when it serves no one place. */\n towns: string\n /** The owner's phone: the preview is texted here, and the site may show it. */\n phone: string\n /** The owner's email: the preview is emailed here, and the site may show it. */\n email: string\n /** Opening hours as the owner says them, e.g. \"Mon–Fri 8–5\". */\n hours: string\n /** The owner's own words about the business, in one box. */\n ownerWords: string\n}\n\nexport type ClientBuildIntakeField = keyof ClientBuildIntake\n\n/** The longest each answer may be, in characters. */\nexport const CLIENT_BUILD_LIMITS: Readonly<Record<ClientBuildIntakeField, number>> = {\n // The three the guided start passes on as its own inputs, which it bounds\n // at 120 (`AI_SITE_INPUT_MAX_CHARS`).\n businessName: 120,\n whatTheyDo: 120,\n towns: 120,\n phone: 32,\n email: 254,\n hours: 200,\n ownerWords: 1_500,\n}\n\n/** An intake with nothing typed: what the screen starts on. */\nexport const EMPTY_CLIENT_BUILD_INTAKE: ClientBuildIntake = {\n businessName: '',\n whatTheyDo: '',\n towns: '',\n phone: '',\n email: '',\n hours: '',\n ownerWords: '',\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Why each answer cannot be used, in a sentence shown beside it; an answer\n * that is fine has no entry. The name and what they do are required, since\n * the site cannot be named or planned without them; the phone and the email\n * are each optional, but the preview needs one of them to reach the owner.\n */\nexport function clientBuildIntakeErrors(\n intake: ClientBuildIntake,\n): Partial<Record<ClientBuildIntakeField, string>> {\n const errors: Partial<Record<ClientBuildIntakeField, string>> = {}\n for (const field of Object.keys(CLIENT_BUILD_LIMITS) as ClientBuildIntakeField[]) {\n if (intake[field].trim().length > CLIENT_BUILD_LIMITS[field]) {\n errors[field] = `Keep this under ${CLIENT_BUILD_LIMITS[field].toLocaleString('en-US')} characters.`\n }\n }\n if (!intake.businessName.trim()) errors.businessName ??= 'Name the business.'\n if (!intake.whatTheyDo.trim()) errors.whatTheyDo ??= 'Say what the business does.'\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n if (phone && !normalizePhone(phone)) errors.phone ??= 'Type a phone number with its area code.'\n if (email && !EMAIL_PATTERN.test(email)) errors.email ??= 'Type an email address like name@example.com.'\n if (!phone && !email) errors.phone ??= 'Add a phone or an email, so the owner gets the preview.'\n return errors\n}\n\n/**\n * An intake as the route admits it: every answer a trimmed string within its\n * bound, or the first reason it is not. Fields the body does not carry are\n * empty, never `undefined`, so the record always has the same shape.\n */\nexport function parseClientBuildIntake(raw: unknown): ClientBuildIntake | string {\n if (!raw || typeof raw !== 'object') return 'Missing the business details'\n const source = raw as Record<string, unknown>\n const intake = { ...EMPTY_CLIENT_BUILD_INTAKE }\n for (const field of Object.keys(CLIENT_BUILD_LIMITS) as ClientBuildIntakeField[]) {\n const value = source[field]\n if (value === undefined || value === null) continue\n if (typeof value !== 'string') return `${field} must be text`\n intake[field] = value.trim()\n }\n const errors = clientBuildIntakeErrors(intake)\n const first = Object.values(errors)[0]\n return first ?? intake\n}\n\n/** The owner's phone as a text goes to it, E.164, or `null` when there is none to text. */\nexport function clientBuildPhone(intake: Pick<ClientBuildIntake, 'phone'>): string | null {\n return normalizePhone(intake.phone)\n}\n\n// ── The domain ─────────────────────────────────────────────────────────────\n\n/**\n * What the owner chose on the Domain step (Zach, 2026-10-19).\n *\n * - `free`: the site stays on its free address for now.\n * - `register`: a name the owner wants to buy. NOT bought here: it rides the\n * bundle link, so the owner buys it on their own phone after they\n * subscribe, with their own registrant details and their own tick on the\n * registrar's terms.\n * - `own`: a domain the owner already has, which the hand-off and the Site\n * Care intake connect (one-click DNS opens the owner's provider and needs\n * their login, so staff do it with them, or hand them the records).\n */\nexport type ClientBuildDomain =\n | { choice: 'free' }\n | { choice: 'register'; name: string; firstYearUsd: number; renewalUsd: number }\n | { choice: 'own'; name: string }\n\n/** A registrable name: labels of letters, digits and hyphens, and a TLD of letters. */\nconst DOMAIN_PATTERN = /^(?=.{3,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$/\n\n/**\n * A domain name as typed, normalized — lower case, no scheme, no `www.`, no\n * path — or `null` when it is not one. \"I have example.com\" is typed on a\n * phone, often as the address bar showed it.\n */\nexport function normalizeClientBuildDomainName(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const name = raw\n .trim()\n .toLowerCase()\n .replace(/^[a-z]+:\\/\\//, '')\n .replace(/^www\\./, '')\n .replace(/[/?#].*$/, '')\n .replace(/\\.$/, '')\n return DOMAIN_PATTERN.test(name) ? name : null\n}\n\nconst isPrice = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value >= 0 && value < 1_000_000\n\n/** A Domain step answer as the route admits it, or the reason it is not one. */\nexport function parseClientBuildDomain(raw: unknown): ClientBuildDomain | string {\n if (!raw || typeof raw !== 'object') return 'Missing the domain choice'\n const source = raw as Record<string, unknown>\n if (source['choice'] === 'free') return { choice: 'free' }\n const name = normalizeClientBuildDomainName(source['name'])\n if (source['choice'] === 'own') {\n return name ? { choice: 'own', name } : 'Type the domain the owner has, like example.com'\n }\n if (source['choice'] === 'register') {\n if (!name) return 'Pick a domain from the search'\n if (!isPrice(source['firstYearUsd']) || !isPrice(source['renewalUsd'])) {\n return 'The domain needs its first-year and renewal prices'\n }\n return {\n choice: 'register',\n name,\n firstYearUsd: source['firstYearUsd'],\n renewalUsd: source['renewalUsd'],\n }\n }\n return 'Unknown domain choice'\n}\n\n/** \"$12.99\" for a whole-dollar-and-cents price. */\nexport function formatDomainUsd(amount: number): string {\n return `$${amount.toFixed(2)}`\n}\n\n/** \"$12.99 the first year, then $18.99 a year\": both prices together, always. */\nexport function domainPriceText(prices: { firstYearUsd: number; renewalUsd: number }): string {\n return prices.firstYearUsd === prices.renewalUsd\n ? `${formatDomainUsd(prices.firstYearUsd)} a year`\n : `${formatDomainUsd(prices.firstYearUsd)} the first year, then ${formatDomainUsd(prices.renewalUsd)} a year`\n}\n\n// ── The intent the hand-off carries ────────────────────────────────────────\n\n/** Add-ons, by their `OrgSeatAddons` key, as Billing sells them. */\nexport type ClientBuildAddon = keyof OrgSeatAddons\n\n/**\n * What the owner is asked to buy, as the build leaves it for the hand-off\n * (AGL-3955, AGL-3956): the plan, the add-ons, the interval, a coupon, and\n * the domain they picked. The hand-off dialog opens on it and the bundle\n * link preselects it, so the owner's checkout is one tap from paid.\n *\n * `domain` is a name to REGISTER, bought by the owner after they subscribe;\n * `existingDomain` is one they already hold, which is connected rather than\n * bought. At most one of the two is set.\n */\nexport interface ClientBuildIntent {\n plan: OrgPlan\n addons: ClientBuildAddon[]\n interval: 'month' | 'year'\n coupon?: string\n domain?: { name: string; firstYearUsd: number; renewalUsd: number }\n existingDomain?: string\n}\n\n/** Site Care rides every build for a client unless staff take it off (AGL-3954). */\nexport const CLIENT_BUILD_DEFAULT_ADDONS: readonly ClientBuildAddon[] = ['siteCare']\n\n/** The plans a build may propose: every self-serve tier but Free. */\nconst INTENT_PLANS: readonly OrgPlan[] = SELF_SERVE_PLANS.filter((plan) => plan !== 'free')\n\n/** The intent a build starts with: the plan the workspace was comped on, Site Care, monthly. */\nexport function clientBuildIntent(options: {\n plan: OrgPlan | null | undefined\n domain?: ClientBuildDomain | null\n}): ClientBuildIntent {\n const plan = options.plan && INTENT_PLANS.includes(options.plan) ? options.plan : 'starter'\n const domain = options.domain\n return {\n plan,\n addons: [...CLIENT_BUILD_DEFAULT_ADDONS],\n interval: 'month',\n ...(domain?.choice === 'register'\n ? { domain: { name: domain.name, firstYearUsd: domain.firstYearUsd, renewalUsd: domain.renewalUsd } }\n : {}),\n ...(domain?.choice === 'own' ? { existingDomain: domain.name } : {}),\n }\n}\n\n/**\n * A stored intent, narrowed, or `null` when it is not one worth acting on.\n * What the hand-off reads: a plan this build cannot sell, an add-on it does\n * not know or a malformed domain reads as absent rather than as a checkout\n * for something that does not exist.\n */\nexport function readClientBuildIntent(raw: unknown): ClientBuildIntent | null {\n if (!raw || typeof raw !== 'object') return null\n const source = raw as Record<string, unknown>\n const plan = source['plan']\n if (typeof plan !== 'string' || !(INTENT_PLANS as readonly string[]).includes(plan)) return null\n const addons = Array.isArray(source['addons'])\n ? source['addons'].filter((addon): addon is ClientBuildAddon => typeof addon === 'string' && addon.length > 0)\n : []\n const interval = source['interval'] === 'year' ? 'year' : 'month'\n const coupon = typeof source['coupon'] === 'string' && source['coupon'].trim() ? source['coupon'].trim() : undefined\n const rawDomain = source['domain']\n const parsed =\n rawDomain && typeof rawDomain === 'object' ? parseClientBuildDomain({ ...rawDomain, choice: 'register' }) : null\n const domain = parsed && typeof parsed === 'object' && parsed.choice === 'register' ? parsed : null\n const existing = normalizeClientBuildDomainName(source['existingDomain'])\n return {\n plan: plan as OrgPlan,\n addons,\n interval,\n ...(coupon ? { coupon } : {}),\n ...(domain\n ? { domain: { name: domain.name, firstYearUsd: domain.firstYearUsd, renewalUsd: domain.renewalUsd } }\n : existing\n ? { existingDomain: existing }\n : {}),\n }\n}\n\n// ── The record ─────────────────────────────────────────────────────────────\n\n/**\n * The build as it is stored, one per workspace, at\n * `orgs/{orgId}/clientBuild/{CLIENT_BUILD_DOC_ID}`: a subcollection the rules\n * open to no client, written and read only by `/api/admin/client-build`\n * (staff) through the Admin SDK. Not on the org document, which every\n * member reads: the owner's phone and email are staff's to hold until the\n * owner joins.\n */\nexport interface ClientBuildRecord {\n intake: ClientBuildIntake\n /** `null` until the Domain step is answered. */\n domain: ClientBuildDomain | null\n intent: ClientBuildIntent\n /** The site the build made, once it has. */\n hostId: string | null\n /** Its subdomain, which is what a console URL names a site by. */\n host: string | null\n /** The guided start's job, once it is started. */\n jobId: string | null\n /**\n * The starter site was given instead of a guided start (no AI on the\n * workspace, or staff chose it), so the build step is finished without a job.\n */\n starter: boolean\n /** When the preview was last sent to the owner, ms; `null` before. */\n previewSentAt: number | null\n updatedAt: number\n /** The staff uid that last wrote it. */\n updatedBy: string\n}\n\n/** The subcollection and document the record lives at under its org. */\nexport const CLIENT_BUILD_COLLECTION = 'clientBuild'\nexport const CLIENT_BUILD_DOC_ID = 'current'\n\n/** The screen's steps, in order. */\nexport const CLIENT_BUILD_STEPS = ['details', 'domain', 'photos', 'build', 'send'] as const\nexport type ClientBuildStep = (typeof CLIENT_BUILD_STEPS)[number]\n\n/**\n * Where a stored build resumes: the first step it has not finished. A phone\n * that reloads mid-build, or a second phone, opens where the first left off.\n */\nexport function clientBuildResumeStep(\n record: Pick<ClientBuildRecord, 'domain' | 'hostId' | 'jobId'> & { starter?: boolean } | null,\n): ClientBuildStep {\n if (!record) return 'details'\n if (!record.domain) return 'domain'\n // The site exists before its photos are asked for, and the guided start\n // waits on them: until a job is started, or the starter site was given\n // instead, the build is at the photos.\n if (!record.hostId || (!record.jobId && !record.starter)) return 'photos'\n return 'send'\n}\n\n/**\n * The address the site is going to live at, as the owner is told it: the\n * domain they picked or already have, else the free address it is on.\n */\nexport function clientBuildSiteAddress(\n domain: ClientBuildDomain | null | undefined,\n liveUrl: string | null,\n): string | null {\n if (domain && domain.choice !== 'free') return domain.name\n return liveUrl ? liveUrl.replace(/^https?:\\/\\//, '') : null\n}\n\n/**\n * The text the owner gets with their preview: short enough for one or two\n * segments, and naming who it is from, since it arrives from a number they\n * do not know. The email says the same at length, from the catalog.\n */\nexport function clientBuildPreviewText(options: {\n businessName: string\n previewUrl: string\n siteAddress: string | null\n productName: string\n}): string {\n const name = options.businessName.trim() || 'your business'\n const address =\n options.siteAddress && options.siteAddress !== options.previewUrl.replace(/^https?:\\/\\//, '')\n ? ` It will live at ${options.siteAddress}.`\n : ''\n return `${options.productName}: the new website for ${name} is ready to look at: ${options.previewUrl}${address}`\n}\n"],"names":["normalizePhone","SELF_SERVE_PLANS","CLIENT_BUILD_LIMITS","businessName","whatTheyDo","towns","phone","email","hours","ownerWords","EMPTY_CLIENT_BUILD_INTAKE","EMAIL_PATTERN","clientBuildIntakeErrors","intake","errors","field","Object","keys","trim","length","toLocaleString","test","parseClientBuildIntake","raw","source","value","undefined","first","values","clientBuildPhone","DOMAIN_PATTERN","normalizeClientBuildDomainName","name","toLowerCase","replace","isPrice","Number","isFinite","parseClientBuildDomain","choice","firstYearUsd","renewalUsd","formatDomainUsd","amount","toFixed","domainPriceText","prices","CLIENT_BUILD_DEFAULT_ADDONS","INTENT_PLANS","filter","plan","clientBuildIntent","options","includes","domain","addons","interval","existingDomain","readClientBuildIntent","Array","isArray","addon","coupon","rawDomain","parsed","existing","CLIENT_BUILD_COLLECTION","CLIENT_BUILD_DOC_ID","CLIENT_BUILD_STEPS","clientBuildResumeStep","record","hostId","jobId","starter","clientBuildSiteAddress","liveUrl","clientBuildPreviewText","address","siteAddress","previewUrl","productName"],"mappings":"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"}
|
|
@@ -43,6 +43,7 @@ export declare enum Route {
|
|
|
43
43
|
ADMIN_EMAILS = "/admin/emails",
|
|
44
44
|
ADMIN_EMAIL_BESIGNER = "/admin/emails/[templateKey]/versions/[versionId]/besigner",
|
|
45
45
|
ORG_HOME = "/[orgSlug]",
|
|
46
|
+
ORG_CLIENT_BUILD = "/[orgSlug]/client-build",
|
|
46
47
|
ORG_MEDIA = "/[orgSlug]/media",
|
|
47
48
|
ORG_DATA = "/[orgSlug]/data",
|
|
48
49
|
ORG_CRM = "/[orgSlug]/crm",
|
|
@@ -190,6 +191,9 @@ export interface RoutePayload {
|
|
|
190
191
|
[Route.ORG_HOME]: {
|
|
191
192
|
orgSlug: string;
|
|
192
193
|
};
|
|
194
|
+
[Route.ORG_CLIENT_BUILD]: {
|
|
195
|
+
orgSlug: string;
|
|
196
|
+
};
|
|
193
197
|
[Route.HOST_DASHBOARD]: {
|
|
194
198
|
orgSlug: string;
|
|
195
199
|
host: string;
|
|
@@ -105,6 +105,8 @@ export var Route = /*#__PURE__*/ function(Route) {
|
|
|
105
105
|
// like the rest of `/admin/*` rather than nested under `[orgSlug]`.
|
|
106
106
|
Route["ADMIN_EMAIL_BESIGNER"] = "/admin/emails/[templateKey]/versions/[versionId]/besigner";
|
|
107
107
|
Route["ORG_HOME"] = "/[orgSlug]";
|
|
108
|
+
// Staff build a site for a client on one phone screen (AGL-3958).
|
|
109
|
+
Route["ORG_CLIENT_BUILD"] = "/[orgSlug]/client-build";
|
|
108
110
|
Route["ORG_MEDIA"] = "/[orgSlug]/media";
|
|
109
111
|
Route["ORG_DATA"] = "/[orgSlug]/data";
|
|
110
112
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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// URL scheme (AGL-621): the org is a first-class path segment `[orgSlug]`\n// so the URL — not client-side precedence — is the source of truth for the\n// active workspace. Host routes nest under `/[orgSlug]/hosts/[host]`; the\n// org area (settings, team, billing, media, data, plugins, support,\n// marketplace) lives directly under `/[orgSlug]`. User-level `manage/*`,\n// staff `admin/*`, and `auth` routes are NOT org-scoped. Hosts stay keyed\n// by doc id here; AGL-622 swaps `[hostId]` for the subdomain slug.\nexport enum Route {\n ADMIN_ORGS = '/admin/orgs',\n ADMIN_ORG_DETAIL = '/admin/orgs/[orgId]',\n ADMIN_ORG_HOST_DETAIL = '/admin/orgs/[orgId]/host/[hostId]',\n // Every site on the platform, across organizations, and one site's staff\n // detail (AGL-3378). The org-nested host route above redirects here.\n ADMIN_SITES = '/admin/sites',\n ADMIN_SITE_DETAIL = '/admin/sites/[hostId]',\n // A site's screen, layout, component, template or form as its draft renders,\n // for staff who are not members of the site (AGL-3378). `kind` is a\n // `PreviewKind`; the version, when not the current one, rides `?version=`.\n ADMIN_SITE_PREVIEW = '/admin/sites/[hostId]/preview/[kind]/[docId]',\n ADMIN_OVERVIEW = '/admin/overview',\n // Staff coupon management (AGL-1105): create discount coupons + promotion\n // codes and read the live net-margin rating before committing to one.\n ADMIN_COUPONS = '/admin/coupons',\n // The panic button (AGL-1501): platform/org/host/user lockdown controls.\n ADMIN_LOCKDOWN = '/admin/lockdown',\n // The narrow lever beside it (AGL-1687): disable or release ONE uploaded\n // file. Its own page rather than a panel on Lockdown because the input is\n // an asset, not a scope — the two forms share no field but the reason.\n ADMIN_MEDIA_QUARANTINE = '/admin/media-quarantine',\n // The INPUT to the two levers above (AGL-1964). Lockdown and quarantine are\n // both things an operator does after learning there is a problem; until\n // this page there was no way for anyone outside the company to say there\n // was one. Reports arrive from the unauthenticated form the tenant runtime\n // serves at /api/report-abuse on every origin.\n ADMIN_ABUSE_REPORTS = '/admin/abuse-reports',\n // The same input, one collection over (AGL-2310). The marketplace's own\n // report button wrote `marketplaceReports` from the day it shipped and\n // nothing read it — every report filed, acknowledged, and unreachable. This\n // is that queue, deliberately shaped like the abuse one rather than as a\n // second vocabulary for the same act.\n // The probes, on a screen (AGL-1900). /api/health/{backups,rate-limits,\n // signups} and /api/admin/email-health each answered a bad-day question to\n // a curl and to nothing else; this is where an operator reads them.\n ADMIN_HEALTH = '/admin/health',\n // The maintenance jobs, on a screen (AGL-1949). audit-archive,\n // reap-plugin-artifacts and reverify-plugin-versions accepted the shared\n // cron secret and nothing else, so the only way to preview or run one was a\n // shell holding the production secret. Separate from Health because Health\n // reads and this acts.\n ADMIN_MAINTENANCE = '/admin/maintenance',\n // The Texas sales tax return (AGL-1900). The figures AGL-1811 computes had\n // only a curl to reach them; filing happens on a fixed quarterly calendar\n // from 2026-09-01, so the return needs a URL a person can bookmark.\n ADMIN_REVENUE = '/admin/revenue',\n // Realised band utilization and the margin that follows from it. Beside\n // Revenue because the two are the halves of one question: that page reports\n // what came in, this one what serving it cost.\n ADMIN_MARGIN_UTILIZATION = '/admin/margin-utilization',\n ADMIN_TAX_RETURN = '/admin/tax-return',\n ADMIN_AUDIT = '/admin/audit',\n ADMIN_USERS = '/admin/users',\n ADMIN_USER_DETAIL = '/admin/users/[uid]',\n ADMIN_FLAGS = '/admin/flags',\n // Platform-wide settings that are not a release flag (AGL-2486). The\n // free-workspace ceiling was the first of them and it opened the\n // Organizations LIST, above the table — a global lever wedged into the\n // screen for browsing individual orgs. Settings that describe the platform\n // rather than one org belong on their own tab, beside the flags.\n ADMIN_SETTINGS = '/admin/settings',\n // Which operator alerts email the operator, immediately or in the daily\n // digest, and whether the channels reach anybody (AGL-3377).\n ADMIN_OPERATOR_ALERTS = '/admin/operator-alerts',\n // One submission or listed plugin in full (AGL-959): the queue index is\n // for scanning, this is where a reviewer reads the manifest, weighs the\n // verifier findings and acts.\n // A page a plugin adds to the staff area (AGL-2939), rendered by the\n // console's generic staff route. The console's own staff routes win the\n // segments they use.\n ADMIN_STAFF_PAGE = '/admin/[staffPage]',\n // Staff support-ticket queue (AGL-849): the operator side of the\n // subscriber `MANAGE_SUPPORT_TICKETS` page — every org's tickets in one place.\n ADMIN_SUPPORT = '/admin/support',\n // Do-not-contact list (AGL-1592): the intake and the queue for Privacy\n // Policy v4 §11's marketing call/text opt-out, and for \"delete the phone\n // number you hold for me\".\n ADMIN_CONTACT_SUPPRESSIONS = '/admin/contact-suppressions',\n ADMIN_EMAILS = '/admin/emails',\n // The one besigner route with no host and no org in it (AGL-749). A system\n // email belongs to the platform, not to a workspace, so it is staff-scoped\n // like the rest of `/admin/*` rather than nested under `[orgSlug]`.\n ADMIN_EMAIL_BESIGNER = '/admin/emails/[templateKey]/versions/[versionId]/besigner',\n ORG_HOME = '/[orgSlug]',\n ORG_MEDIA = '/[orgSlug]/media',\n ORG_DATA = '/[orgSlug]/data',\n /*\n * The organization-level CRM hub (AGL-2630): the same sections the site hub\n * has — `/[orgSlug]/crm/<section>[/<recordId>]` — mounted with an ORG scope,\n * for an org-wide member who works every site's people at once. A bare\n * `/crm` lands on the first section the reader may open, as the site hub's\n * does. What this route adds over the site hub is the cross-site fact the\n * old address book carried — which sites know a person — and what it takes\n * from it is the site every create must name, since a record is always\n * captured BY a site. Served by `ORG_PLUGIN` below, from the CRM's own\n * `orgNavItems`; the constant is the address its links are built from.\n */\n ORG_CRM = '/[orgSlug]/crm',\n /*\n * The generic ORGANIZATION-level plugin route (AGL-2974): every surface an\n * extension declares in `orgNavItems` is served here, at\n * `/[orgSlug]/<href>[/<section>…]`, by\n * `app/(app)/[orgSlug]/[...pluginSlug]/page.tsx`. The org twin of\n * `HOST_PLUGIN`, and like it the one org route whose leaf segment is data:\n * every named org route in this enum wins over it.\n */\n ORG_PLUGIN = '/[orgSlug]/[pluginSlug]',\n ORG_PLUGINS = '/[orgSlug]/plugins',\n // One plugin, as installed in this workspace (AGL-1007): scope, settings\n // and permissions in one place. The segment takes EITHER identifier\n // (AGL-1010) — a listing id for a marketplace install, a registry id for a\n // first-party plugin — hence `pluginRef` rather than `listingId`.\n ORG_PLUGIN_INSTALLATION = '/[orgSlug]/plugins/[pluginRef]',\n // Org-scope marketplace (AGL-772): the single place to browse, view and\n // install marketplace items. The per-site tab it replaced, and the retired\n // `/[orgSlug]/marketplace` seller area, were removed with the rest of the\n // `marketplace` naming in AGL-975 — the word is being freed for a public\n // forum, and a redirect stub sitting on `/marketplace` would have taken the\n // one path that feature wants.\n /*\n * The marketplace's own SECTIONS are not here any more (AGL-3080). Eight\n * of them — browse, licences, upload, the four seller cards and the\n * publish form — were hand-written console routes, and became an\n * `orgNavItems` declaration whose `sections` name those segments once, in\n * the plugin. Restating them here was a second copy of a list the plugin\n * owns, and every entry had zero callers the moment the routes went.\n *\n * What stays is what the CONSOLE still builds: the surface itself, a\n * listing, a publisher, and the installed tab that the org plugins card\n * links across to. A console page linking into a plugin's surface is the\n * ordinary case; a console page enumerating that surface's sections is\n * not.\n */\n ORG_MARKETPLACE = '/[orgSlug]/marketplace',\n /*\n * Marketplace SECTIONS are routes (AGL-2501), for the reason the settings\n * and account sections are: a panel that is not open should cost nothing,\n * and the seller half of this hub reads the organization's REVENUE.\n *\n * The four seller sections are the reason the layout gates rather than the\n * rail. As tabs they were simply not rendered for a member without\n * `publishToMarketplace`; as routes each one is reachable by typing its URL,\n * so the refusal has to sit above them all rather than in the list that\n * draws them.\n *\n * Every segment here is shorter than the ten characters `createResourceUid`\n * emits, so none of them can ever shadow a real `[listingId]` sitting at the\n * same level. `upload` rather than `publish` because `publish/plugin` is\n * already a route beneath this one, and two directories contributing the\n * same segment is a tree Next cannot resolve a layout for.\n */\n ORG_MARKETPLACE_INSTALLED = '/[orgSlug]/marketplace/installed',\n ORG_MARKETPLACE_LISTING = '/[orgSlug]/marketplace/[listingId]',\n ORG_MARKETPLACE_PUBLISHER = '/[orgSlug]/marketplace/publisher/[handle]',\n // Publishing a plugin is a page, not a modal (AGL-1078): the most\n // consequential thing a publisher does now has a URL, so it can be linked,\n // reloaded, and reached from a listing to ship an update.\n ORG_SETTINGS = '/[orgSlug]/settings',\n /*\n * Settings SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening General also mounted the API-keys, SSO and data-export cards and\n * ran every read in them. Routes mount one page and code-split per route.\n * `ORG_SETTINGS` itself redirects to General.\n */\n ORG_SETTINGS_GENERAL = '/[orgSlug]/settings/general',\n ORG_SETTINGS_PROFILE = '/[orgSlug]/settings/profile',\n ORG_SETTINGS_PLUGINS = '/[orgSlug]/settings/plugins',\n ORG_SETTINGS_API_KEYS = '/[orgSlug]/settings/api-keys',\n ORG_SETTINGS_BRANDING = '/[orgSlug]/settings/branding',\n ORG_SETTINGS_SSO = '/[orgSlug]/settings/sso',\n ORG_SETTINGS_OWNERSHIP = '/[orgSlug]/settings/ownership',\n /**\n * Export the people a workspace holds and file a person's erasure\n * (AGL-2839) — outside the CRM, so it is there on every plan.\n */\n ORG_SETTINGS_PRIVACY = '/[orgSlug]/settings/privacy',\n // Holds & reviews (AGL-3368): everything on the workspace that was held,\n // flagged, locked or paused, its status, and the way to request a review.\n // Where every risk notice's \"Request a review\" lands (`?notice=<id>`).\n ORG_SETTINGS_HOLDS = '/[orgSlug]/settings/holds',\n // Import & export (AGL-3535): every resource the workspace can move,\n // workspace packages, and the history of its imports with undo.\n ORG_SETTINGS_DATA = '/[orgSlug]/settings/data',\n ORG_SETTINGS_DELETE = '/[orgSlug]/settings/delete',\n // The `orgSettings` plugin zone's own section (AGL-3711): offered only\n // while a plugin has a card for it.\n ORG_SETTINGS_INTEGRATIONS = '/[orgSlug]/settings/integrations',\n /**\n * Billing, section by section (AGL-2501).\n *\n * `MANAGE_BILLING` is unchanged and is the PLAN section — it resolves to a\n * page inside a route group, which adds no path segment. That is what makes\n * this split free: every link in the console, every hash anchor\n * (`#addons`, `#collaborator-seats`) and Stripe's own dunning mail all point\n * here already and none of them moves. No redirect, and no `?tab=` shim to\n * carry forever.\n */\n MANAGE_BILLING = '/[orgSlug]/billing',\n MANAGE_BILLING_USAGE = '/[orgSlug]/billing/usage',\n MANAGE_BILLING_INVOICES = '/[orgSlug]/billing/invoices',\n MANAGE_BILLING_SETTINGS = '/[orgSlug]/billing/settings',\n // The ONE billing URL that carries no org (AGL-2430).\n //\n // Everything else on this table is org-scoped, which is correct for a\n // console and wrong for the only caller that cannot template a URL:\n // Stripe's dunning emails. \"Payment method updates\" takes a single static\n // link for every customer in the account — there is no `{{org}}` to\n // interpolate — so a table with no org-agnostic billing entry means those\n // emails can only ever point at a page that is not billing. Today all four\n // of them point at the marketing homepage, which is how a failed card\n // becomes a cancelled subscription with no reachable way to fix it.\n //\n // This resolves the org from the SESSION instead of from the URL: one\n // workspace goes straight through, several offer a choice, none says so.\n // `billing` is a reserved subdomain (`RESERVED_SUBDOMAINS`), so no org can\n // hold this slug and the literal segment can never shadow a real\n // `/[orgSlug]`.\n BILLING_ENTRY = '/billing',\n MANAGE_USER_SETTINGS = '/manage/user',\n /*\n * Account SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening Account also mounted the email-addresses, passkeys, recent\n * sign-ins, data-export and close-account cards and ran every read in them.\n * Routes mount one page and code-split per route.\n *\n * The segments are the ids the panels carried, because a transactional\n * email already in people's inboxes links `/manage/user?tab=security`:\n * `MANAGE_USER_SETTINGS` forwards each of those ids to the section that\n * holds it, and the segment matching the id is what keeps that map a\n * one-liner rather than a second naming scheme.\n */\n MANAGE_USER_ACCOUNT = '/manage/user/account',\n MANAGE_USER_EMAILS = '/manage/user/emails',\n MANAGE_USER_PROFILE = '/manage/user/profile',\n MANAGE_USER_BASIC = '/manage/user/basic',\n MANAGE_USER_SECURITY = '/manage/user/security',\n MANAGE_USER_CLOSE = '/manage/user/close',\n MANAGE_NOTIFICATIONS = '/manage/notifications',\n /*\n * The preferences, off the feed (AGL-3226).\n *\n * They lived in the header of the feed itself, which was already the\n * tallest thing on that page before per-channel and per-scope settings\n * multiplied them — and it put an edit form above the list it edits, which\n * the console does nowhere else.\n *\n * A CHILD of the feed rather than a sibling under `/manage`, because the\n * relationship is exactly that: it configures what the page above it shows.\n * That also keeps `/manage/notifications` the address in the transactional\n * mail already in people's inboxes, which keeps working and now carries a\n * button to here.\n */\n MANAGE_NOTIFICATION_SETTINGS = '/manage/notifications/settings',\n /*\n * A URL for the defect channel (AGL-2486). \"Report an issue\" was reachable\n * only by opening the account menu, so nothing outside the console could\n * send anyone to it — the repo's README pointed at GitHub issues, which we\n * do not use, and every other \"tell us about it\" surface had the same\n * problem. User-level rather than org-scoped because the dialog is: it\n * needs no workspace, no role and no paid plan.\n */\n MANAGE_REPORT_ISSUE = '/manage/report-issue',\n AUTH_SIGN_IN = '/signin',\n AUTH_SIGN_OUT = '/signout',\n AUTH_SIGN_UP = '/signup',\n AUTH_VERIFY_EMAIL = '/verify-email',\n HOST_LIST = '/[orgSlug]/hosts',\n HOST_CONTENT = '/[orgSlug]/hosts/[host]/content',\n // The collection is a PATH SEGMENT, not `?collection=` (AGL-2498).\n //\n // It was a query parameter because the manager began as one page with a\n // Select on it, and the Select was the only thing that knew which\n // collection you were looking at. That is the wrong owner: which collection\n // is open is the page's IDENTITY, not a filter applied to it. As a\n // parameter it could be dropped by any link that rebuilt the query, it\n // sorted below `?tab=` in a URL nobody could read, and — the reason this\n // moved — it left the entry beneath it with nowhere to hang: an entry is\n // addressed `collection + entry`, and half of that address cannot live in\n // the path while the other half lives in the query.\n //\n // The segment is the collection's SLUG, not its document id, and that is a\n // correction rather than a preference. Collection document ids are not\n // uniform — the seeded ones were given readable ids (`blog`, `changelog`)\n // and everything created since gets a `createResourceUid`, so routing by id\n // produced `/content/changelog` beside `/content/QgXv7lU_rG` on ONE site.\n //\n // A slug is the right key on the merits too: it is unique per host and kind\n // (claimed in a transaction by /api/hosts/collections), it is the\n // collection's own public address, and it is the thing an author recognises.\n // A document id is still ACCEPTED and rewritten to the slug form, so no\n // existing link breaks.\n //\n // `?collection=` still resolves the same way (AGL-845's DAM deep links are\n // in the wild).\n HOST_CONTENT_COLLECTION = '/[orgSlug]/hosts/[host]/content/[collectionSlug]',\n // One entry's detail page (AGL-2498). `entries` is a LITERAL segment rather\n // than `content/[collectionId]/[entryId]`, and it is load-bearing twice\n // over: it mirrors the Firestore path the entry actually lives at\n // (`collections/{c}/entries/{e}`), and it keeps the id space clear so a\n // later sibling — a collection's own settings page, say — cannot be\n // shadowed by an entry that happens to be called the same thing.\n //\n // `[entryId]` also carries the sentinel `new`, which is a draft that has no\n // document yet. Entry ids come from `createResourceUid`, so nothing real\n // can collide with it.\n CONTENT_ENTRY_DETAILS = '/[orgSlug]/hosts/[host]/content/[collectionSlug]/entries/[entryId]',\n MANAGE_TEAM = '/[orgSlug]/team',\n /*\n * Team SECTIONS are routes, not tabs (AGL-2501). A section's bundle then\n * arrives when a reader opens it and never before, the section is linkable,\n * and the active state is a fact about the URL rather than state that has to\n * be kept in sync with it. `MANAGE_TEAM` itself redirects to members.\n */\n MANAGE_TEAM_MEMBERS = '/[orgSlug]/team/members',\n MANAGE_TEAM_ROLES = '/[orgSlug]/team/roles',\n MANAGE_TEAM_ACTIVITY = '/[orgSlug]/team/activity',\n MANAGE_TEAM_MEMBER = '/[orgSlug]/team/[uid]',\n // Support is an UMBRELLA, not a page (AGL-1158). The two channels beneath\n // it are separate features that were only ever one screen: they are gated\n // differently (tickets need a first-response commitment, from Pro; the\n // forum is open to every tier including Free) and they fail independently.\n // Sharing one route made them share a blast radius — AGL-1157 was one bad\n // line in the common loader that emptied BOTH lists at once.\n //\n // `MANAGE_SUPPORT` itself stays, and stays the nav tab's href: it now\n // forwards to whichever channel the org's tier makes primary, so a\n // forum-only workspace lands on a whole forum instead of a half-empty page\n // beside a ticket card it may not use.\n MANAGE_SUPPORT = '/[orgSlug]/support',\n MANAGE_SUPPORT_TICKETS = '/[orgSlug]/support/tickets',\n MANAGE_SUPPORT_FORUM = '/[orgSlug]/support/forum',\n /*\n * The ONE support URL that carries no org (AGL-3265), and the second member\n * of the apex category `BILLING_ENTRY` opened.\n *\n * Everything a person is handed when they need help — a receipt, a system\n * email footer, a Stripe customer portal, a self-host runbook — stores ONE\n * link for every customer, with no workspace to interpolate. `/[orgSlug]`\n * cannot be templated into any of them, so the only support address those\n * surfaces could carry was one outside the console entirely.\n *\n * This resolves the workspace from the SESSION instead: one workspace goes\n * straight through, several offer a choice, none says so. It forwards to\n * `MANAGE_SUPPORT`, which is itself an umbrella — so the tier decision about\n * tickets versus the forum stays in the one place that already makes it.\n *\n * `support` is a reserved subdomain (`RESERVED_SUBDOMAINS` in\n * `host-naming.ts`), so no org can hold this slug and the literal segment\n * can never shadow a real `/[orgSlug]`. That is the same guarantee\n * `BILLING_ENTRY` rests on, and it is a precondition rather than a\n * coincidence: an apex route whose segment an org could claim would take a\n * workspace's own address away from it.\n */\n SUPPORT_ENTRY = '/support',\n HOST_DASHBOARD = '/[orgSlug]/hosts/[host]',\n // The catch-all console page a plugin's own nav items resolve against\n // (`app/(app)/[orgSlug]/hosts/[host]/[pluginSlug]/page.tsx`). Plugin slugs\n // are open-ended, so this is the one route whose leaf segment is data.\n HOST_PLUGIN = '/[orgSlug]/hosts/[host]/[pluginSlug]',\n HOST_INBOX = '/[orgSlug]/hosts/[host]/inbox',\n HOST_MEDIA = '/[orgSlug]/hosts/[host]/media',\n HOST_SETUP = '/[orgSlug]/hosts/[host]/setup',\n /*\n * Setup SECTIONS are routes (AGL-2501), so an unopened one costs neither a\n * read nor a byte. `HOST_SETUP` stays the nav tab's href and redirects to\n * Basic details, honouring the `?tab=` ids these sections were deep-linked\n * by — unlike the settings and marketplace hubs, links holding those ids are\n * demonstrably in the wild and two are built in this repo.\n */\n HOST_SETUP_DETAILS = '/[orgSlug]/hosts/[host]/setup/details',\n HOST_SETUP_SEO = '/[orgSlug]/hosts/[host]/setup/seo',\n HOST_SETUP_TRACKING = '/[orgSlug]/hosts/[host]/setup/tracking',\n HOST_SETUP_THEME = '/[orgSlug]/hosts/[host]/setup/theme',\n HOST_SETUP_BUSINESS = '/[orgSlug]/hosts/[host]/setup/business',\n HOST_SETUP_EMAILS = '/[orgSlug]/hosts/[host]/setup/emails',\n // The `hostSettings` plugin zone's own section (AGL-3711): offered only\n // while a plugin has a card for it.\n HOST_SETUP_INTEGRATIONS = '/[orgSlug]/hosts/[host]/setup/integrations',\n // Host Admin area (AGL-1014): owner/admin-only controls — per-site plugin\n // enablement and the Danger zone — out of the Setup page collaborators\n // legitimately visit.\n HOST_ADMIN = '/[orgSlug]/hosts/[host]/admin',\n /*\n * Site-admin SECTIONS are routes (AGL-2501), so an unopened one costs neither\n * a read nor a byte. `HOST_ADMIN` redirects to Plugins and still honours the\n * `?tab=` ids these sections were deep-linked by.\n */\n /*\n * The site as an OBJECT: its name in the console and the subdomain it\n * answers to. First in the rail because an address and a name are what\n * identify the thing every other section governs.\n */\n HOST_ADMIN_GENERAL = '/[orgSlug]/hosts/[host]/admin/general',\n HOST_ADMIN_PLUGINS = '/[orgSlug]/hosts/[host]/admin/plugins',\n /*\n * One plugin, as installed on ONE site (AGL-428, AGL-1014) — the\n * site-scoped twin of `ORG_PLUGIN_INSTALLATION`, reached by clicking a row\n * on the site's plugin\n * list exactly as the workspace list opens its own detail page. The segment\n * is `[pluginRef]` and accepts either identifier for the same reason the\n * workspace route does (AGL-1010): a marketplace install is keyed by listing\n * id, a first-party plugin by its registry id, and an admin does not think\n * of the two as different things.\n */\n HOST_ADMIN_PLUGIN = '/[orgSlug]/hosts/[host]/admin/plugins/[pluginRef]',\n HOST_ADMIN_DOMAIN = '/[orgSlug]/hosts/[host]/admin/domain',\n HOST_ADMIN_SECURITY = '/[orgSlug]/hosts/[host]/admin/security',\n /*\n * The designed screen behind each status code, and the switch that shows the\n * 503 one at every address at once (AGL-3178).\n *\n * Beside Security because both answer what a visitor's browser is handed on\n * a page the site did not design for them, and above Activity because it is\n * a control rather than a record.\n */\n HOST_ADMIN_ERROR_PAGES = '/[orgSlug]/hosts/[host]/admin/error-pages',\n HOST_ADMIN_ACTIVITY = '/[orgSlug]/hosts/[host]/admin/activity',\n /*\n * Backup, restore and publishing the site as a template — moving the whole\n * site somewhere else, or bringing it back. Its own section rather than the\n * Danger zone: restore overwrites, but export and publishing a template do\n * not, and filing all three under a destructive heading mislabels two of\n * them.\n */\n HOST_ADMIN_BACKUP = '/[orgSlug]/hosts/[host]/admin/backup',\n HOST_ADMIN_DANGER = '/[orgSlug]/hosts/[host]/admin/danger',\n HOST_THEME = '/[orgSlug]/hosts/[host]/theme',\n /*\n * The automation section: Workflows, Actions and Webhooks under one parent.\n * The parent is named for what the three have in common, so no tab shares a\n * name with the section that contains it.\n */\n HOST_AUTOMATION = '/[orgSlug]/hosts/[host]/automation',\n HOST_DATA = '/[orgSlug]/hosts/[host]/data',\n HOST_LOGIC = '/[orgSlug]/hosts/[host]/logic',\n HOST_COMPONENTS = '/[orgSlug]/hosts/[host]/components',\n // Component detail (AGL-693): the listing links here, and the besigner is\n // reached from here — matching SCREEN_DETAILS rather than jumping a row\n // straight into the editor.\n COMPONENT_DETAILS = '/[orgSlug]/hosts/[host]/components/[componentId]',\n HOST_FORMS = '/[orgSlug]/hosts/[host]/forms',\n // A form is a designable document, so its surface is the component one:\n // a list, a detail page that owns the versions, and the besigner reached\n // from there. Deliberately NOT a row that jumps into the editor — a form's\n // routing and consent field are edited on the detail page, and those decide\n // what the design is then checked against.\n FORM_DETAILS = '/[orgSlug]/hosts/[host]/forms/[formId]',\n HOST_TEMPLATES = '/[orgSlug]/hosts/[host]/templates',\n // Template detail (AGL-694), the counterpart to COMPONENT_DETAILS. Note\n // TEMPLATE_BESIGNER carries no versionId — templates version but never\n // publish, so there is no \"current\" pointer to route through.\n TEMPLATE_DETAILS = '/[orgSlug]/hosts/[host]/templates/[templateId]',\n HOST_MARKETING = '/[orgSlug]/hosts/[host]/marketing',\n // Events now come from the events-calendar plugin, served by the generic\n // `[orgSlug]/hosts/[host]/[pluginSlug]` route (AGL-394).\n HOST_REDIRECTS = '/[orgSlug]/hosts/[host]/redirects',\n HOST_USERS = '/[orgSlug]/hosts/[host]/users',\n HOST_ANALYTICS = '/[orgSlug]/hosts/[host]/analytics',\n COMPONENT_BESIGNER = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/besigner',\n /*\n * A document a plugin keeps under a site and authors in the besigner\n * (`plugin-manager/besigner-documents.ts`): the segment is the kind the\n * plugin declared, the details page is the plugin's own at\n * `<segment>/<docId>`, and the editor and preview are the console's.\n */\n PLUGIN_DOCUMENT_DETAILS = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]',\n PLUGIN_DOCUMENT_BESIGNER = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/besigner',\n TEMPLATE_BESIGNER = '/[orgSlug]/hosts/[host]/templates/[templateId]/besigner',\n LAYOUT_BESIGNER = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/besigner',\n // Draft preview for every besigner document kind (AGL-1203). Screens had\n // the only preview route; components, layouts and templates showed a\n // Preview button that did nothing. Each renders the same localStorage\n // snapshot through the site renderer, so they work on localhost with no\n // deployment. TEMPLATE_PREVIEW carries no versionId, matching\n // TEMPLATE_BESIGNER.\n COMPONENT_PREVIEW = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/preview',\n PLUGIN_DOCUMENT_PREVIEW = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/preview',\n TEMPLATE_PREVIEW = '/[orgSlug]/hosts/[host]/templates/[templateId]/preview',\n LAYOUT_PREVIEW = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/preview',\n // The list sits at the bare path, like HOST_COMPONENTS. It used to be\n // `/layouts/list`: the pages router had a `layouts/index.tsx` that\n // redirected to `layouts/list.tsx`, and the App Router migration carried\n // the pair over verbatim. Components, added later, had no such legacy and\n // put its list at the bare path — which is the shape we want everywhere.\n // `/layouts/list` still serves the same page so old links survive — an\n // alias, not a redirect, because the bare path used to answer a cached 308\n // pointing at `/list` and redirecting back would loop. See the comment in\n // `app/(app)/[orgSlug]/hosts/[host]/screens/list/page.tsx`.\n HOST_LAYOUTS = '/[orgSlug]/hosts/[host]/layouts',\n // Layout detail (AGL-695), completing the list → detail → besigner shape\n // across screens, components, templates and layouts.\n LAYOUT_DETAILS = '/[orgSlug]/hosts/[host]/layouts/[layoutId]',\n SCREEN_BESIGNER = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/besigner',\n // Per-site email besigner (AGL-770): a site owner designs a transactional\n // email their site sends. The host-scoped sibling of the staff system-email\n // editor (ADMIN_EMAIL_BESIGNER).\n HOST_EMAIL_BESIGNER = '/[orgSlug]/hosts/[host]/emails/[templateKey]/versions/[versionId]/besigner',\n SCREEN_DETAILS = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/view',\n SCREEN_PREVIEW = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/preview',\n // Bare path, matching HOST_LAYOUTS and HOST_COMPONENTS — see the note on\n // HOST_LAYOUTS. `/screens/list` still serves the same page as an alias.\n HOST_SCREENS = '/[orgSlug]/hosts/[host]/screens',\n}\n\n/**\n * Params required to build each {@link Route}.\n *\n * Deliberately NOT extended from an index signature (AGL-685). It used to be\n * `extends Record<keyof any, any>`, which meant any `Route` missing an entry\n * here silently typed its payload as `any` — `Route.HOST_SETUP` was one of\n * them, and `buildRoute(Route.HOST_SETUP, { hostId })` compiled cleanly while\n * emitting `/<orgSlug?>/hosts/<host?>/setup`. Without the index signature a\n * new `Route` with no entry is a compile error, which is the whole point of\n * the payload being required in the first place.\n */\nexport interface RoutePayload {\n [Route.AUTH_SIGN_UP]: undefined\n [Route.AUTH_SIGN_IN]: undefined\n [Route.AUTH_SIGN_OUT]: undefined\n [Route.AUTH_VERIFY_EMAIL]: undefined\n [Route.SCREEN_BESIGNER]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.ORG_HOME]: { orgSlug: string }\n [Route.HOST_DASHBOARD]: { orgSlug: string; host: string }\n [Route.ADMIN_ORGS]: undefined\n [Route.ADMIN_ORG_DETAIL]: { orgId: string }\n [Route.ADMIN_ORG_HOST_DETAIL]: { orgId: string; hostId: string }\n [Route.ADMIN_SITES]: undefined\n [Route.ADMIN_SITE_DETAIL]: { hostId: string }\n [Route.ADMIN_SITE_PREVIEW]: { hostId: string; kind: string; docId: string }\n [Route.ADMIN_OVERVIEW]: undefined\n [Route.ADMIN_COUPONS]: undefined\n [Route.ADMIN_LOCKDOWN]: undefined\n [Route.ADMIN_MEDIA_QUARANTINE]: undefined\n [Route.ADMIN_ABUSE_REPORTS]: undefined\n [Route.ADMIN_HEALTH]: undefined\n [Route.ADMIN_MAINTENANCE]: undefined\n [Route.ADMIN_REVENUE]: undefined\n [Route.ADMIN_MARGIN_UTILIZATION]: undefined\n [Route.ADMIN_TAX_RETURN]: undefined\n [Route.ADMIN_AUDIT]: undefined\n [Route.ADMIN_USERS]: undefined\n [Route.ADMIN_USER_DETAIL]: { uid: string }\n [Route.ADMIN_FLAGS]: undefined\n [Route.ADMIN_SETTINGS]: undefined\n [Route.ADMIN_OPERATOR_ALERTS]: undefined\n [Route.ADMIN_STAFF_PAGE]: { staffPage: string }\n [Route.ADMIN_SUPPORT]: undefined\n [Route.ADMIN_CONTACT_SUPPRESSIONS]: undefined\n [Route.ADMIN_EMAILS]: undefined\n [Route.ADMIN_EMAIL_BESIGNER]: { templateKey: string; versionId: string }\n [Route.HOST_EMAIL_BESIGNER]: {\n orgSlug: string\n host: string\n templateKey: string\n versionId: string\n }\n [Route.ORG_MEDIA]: { orgSlug: string }\n [Route.ORG_DATA]: { orgSlug: string }\n [Route.ORG_CRM]: { orgSlug: string }\n [Route.ORG_PLUGIN]: { orgSlug: string; pluginSlug: string }\n [Route.ORG_PLUGINS]: { orgSlug: string }\n [Route.ORG_PLUGIN_INSTALLATION]: { orgSlug: string; pluginRef: string }\n [Route.ORG_MARKETPLACE]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_INSTALLED]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_LISTING]: { orgSlug: string; listingId: string }\n [Route.ORG_MARKETPLACE_PUBLISHER]: { orgSlug: string; handle: string }\n [Route.MANAGE_USER_SETTINGS]: undefined\n [Route.MANAGE_USER_ACCOUNT]: undefined\n [Route.MANAGE_USER_EMAILS]: undefined\n [Route.MANAGE_USER_PROFILE]: undefined\n [Route.MANAGE_USER_BASIC]: undefined\n [Route.MANAGE_USER_SECURITY]: undefined\n [Route.MANAGE_USER_CLOSE]: undefined\n [Route.MANAGE_NOTIFICATIONS]: undefined\n [Route.MANAGE_NOTIFICATION_SETTINGS]: undefined\n [Route.MANAGE_REPORT_ISSUE]: undefined\n [Route.ORG_SETTINGS]: { orgSlug: string }\n [Route.ORG_SETTINGS_GENERAL]: { orgSlug: string }\n [Route.ORG_SETTINGS_PROFILE]: { orgSlug: string }\n [Route.ORG_SETTINGS_PLUGINS]: { orgSlug: string }\n [Route.ORG_SETTINGS_API_KEYS]: { orgSlug: string }\n [Route.ORG_SETTINGS_BRANDING]: { orgSlug: string }\n [Route.ORG_SETTINGS_SSO]: { orgSlug: string }\n [Route.ORG_SETTINGS_OWNERSHIP]: { orgSlug: string }\n [Route.ORG_SETTINGS_PRIVACY]: { orgSlug: string }\n [Route.ORG_SETTINGS_HOLDS]: { orgSlug: string }\n [Route.ORG_SETTINGS_DATA]: { orgSlug: string }\n [Route.ORG_SETTINGS_DELETE]: { orgSlug: string }\n [Route.ORG_SETTINGS_INTEGRATIONS]: { orgSlug: string }\n [Route.HOST_LIST]: { orgSlug: string }\n [Route.HOST_CONTENT]: { orgSlug: string; host: string }\n [Route.HOST_CONTENT_COLLECTION]: {\n orgSlug: string\n host: string\n /** The collection's slug — see the route. A document id also resolves. */\n collectionSlug: string\n }\n [Route.CONTENT_ENTRY_DETAILS]: {\n orgSlug: string\n host: string\n collectionSlug: string\n /**\n * An entry id, or the literal `new` for a draft with no document yet.\n *\n * An ID here while the collection above is a SLUG, deliberately. An entry\n * slug is unique only WITHIN its collection and it is editable on the\n * detail page — so routing by it would move the console address out from\n * under someone in the middle of typing in the slug field, and a draft has\n * no slug at all to route by. The entry id is also what every activity\n * record and the DAM's \"Used on\" list already carry.\n */\n entryId: string\n }\n [Route.MANAGE_TEAM]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBERS]: { orgSlug: string }\n [Route.MANAGE_TEAM_ROLES]: { orgSlug: string }\n [Route.MANAGE_TEAM_ACTIVITY]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBER]: { orgSlug: string; uid: string }\n [Route.MANAGE_SUPPORT]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_TICKETS]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_FORUM]: { orgSlug: string }\n [Route.MANAGE_BILLING]: { orgSlug: string }\n [Route.MANAGE_BILLING_USAGE]: { orgSlug: string }\n [Route.MANAGE_BILLING_INVOICES]: { orgSlug: string }\n [Route.MANAGE_BILLING_SETTINGS]: { orgSlug: string }\n [Route.BILLING_ENTRY]: undefined\n [Route.SUPPORT_ENTRY]: undefined\n [Route.HOST_INBOX]: { orgSlug: string; host: string }\n [Route.HOST_SETUP]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_DETAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_SEO]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_TRACKING]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_THEME]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_BUSINESS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_EMAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_INTEGRATIONS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_GENERAL]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGINS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGIN]: { orgSlug: string; host: string; pluginRef: string }\n [Route.HOST_ADMIN_DOMAIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_SECURITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ERROR_PAGES]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ACTIVITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_BACKUP]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_DANGER]: { orgSlug: string; host: string }\n [Route.HOST_PLUGIN]: { orgSlug: string; host: string; pluginSlug: string }\n [Route.HOST_MEDIA]: { orgSlug: string; host: string }\n [Route.HOST_THEME]: { orgSlug: string; host: string }\n [Route.HOST_AUTOMATION]: { orgSlug: string; host: string }\n [Route.HOST_DATA]: { orgSlug: string; host: string }\n [Route.HOST_LOGIC]: { orgSlug: string; host: string }\n [Route.HOST_COMPONENTS]: { orgSlug: string; host: string }\n [Route.COMPONENT_DETAILS]: {\n orgSlug: string\n host: string\n componentId: string\n }\n [Route.HOST_FORMS]: { orgSlug: string; host: string }\n [Route.FORM_DETAILS]: {\n orgSlug: string\n host: string\n formId: string\n }\n [Route.HOST_TEMPLATES]: { orgSlug: string; host: string }\n [Route.TEMPLATE_DETAILS]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.HOST_MARKETING]: { orgSlug: string; host: string }\n [Route.HOST_REDIRECTS]: { orgSlug: string; host: string }\n [Route.HOST_USERS]: { orgSlug: string; host: string }\n [Route.HOST_ANALYTICS]: { orgSlug: string; host: string }\n [Route.COMPONENT_BESIGNER]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_DETAILS]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n }\n [Route.PLUGIN_DOCUMENT_BESIGNER]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_BESIGNER]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_BESIGNER]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.COMPONENT_PREVIEW]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_PREVIEW]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_PREVIEW]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_PREVIEW]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.HOST_LAYOUTS]: { orgSlug: string; host: string }\n [Route.LAYOUT_DETAILS]: { orgSlug: string; host: string; layoutId: string }\n [Route.SCREEN_DETAILS]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.SCREEN_PREVIEW]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.HOST_SCREENS]: { orgSlug: string; host: string }\n}\n\nexport const routeReplacePattern = /\\[([^\\]]+)\\]/g\n\n/**\n * Builds a concrete path from a {@link Route} template. The payload is\n * REQUIRED whenever the route declares params (e.g. `orgSlug`, `hostId`) and\n * omitted only for param-less routes — so a forgotten `orgSlug` is a compile\n * error, not a `/<orgSlug?>/…` link that breaks at runtime (AGL-621).\n */\nexport function buildRoute<Tmpl extends Route>(\n template: Tmpl,\n ...[payload]: RoutePayload[Tmpl] extends undefined\n ? [payload?: undefined]\n : [payload: RoutePayload[Tmpl]]\n) {\n return template.replace(routeReplacePattern, (match, key) => {\n const value = (payload as Record<string, unknown> | undefined)?.[key]\n return value != null ? String(value) : `<${key}?>`\n })\n}\n"],"names":["Route","routeReplacePattern","buildRoute","template","payload","replace","match","key","value","String"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/console-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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// URL scheme (AGL-621): the org is a first-class path segment `[orgSlug]`\n// so the URL — not client-side precedence — is the source of truth for the\n// active workspace. Host routes nest under `/[orgSlug]/hosts/[host]`; the\n// org area (settings, team, billing, media, data, plugins, support,\n// marketplace) lives directly under `/[orgSlug]`. User-level `manage/*`,\n// staff `admin/*`, and `auth` routes are NOT org-scoped. Hosts stay keyed\n// by doc id here; AGL-622 swaps `[hostId]` for the subdomain slug.\nexport enum Route {\n ADMIN_ORGS = '/admin/orgs',\n ADMIN_ORG_DETAIL = '/admin/orgs/[orgId]',\n ADMIN_ORG_HOST_DETAIL = '/admin/orgs/[orgId]/host/[hostId]',\n // Every site on the platform, across organizations, and one site's staff\n // detail (AGL-3378). The org-nested host route above redirects here.\n ADMIN_SITES = '/admin/sites',\n ADMIN_SITE_DETAIL = '/admin/sites/[hostId]',\n // A site's screen, layout, component, template or form as its draft renders,\n // for staff who are not members of the site (AGL-3378). `kind` is a\n // `PreviewKind`; the version, when not the current one, rides `?version=`.\n ADMIN_SITE_PREVIEW = '/admin/sites/[hostId]/preview/[kind]/[docId]',\n ADMIN_OVERVIEW = '/admin/overview',\n // Staff coupon management (AGL-1105): create discount coupons + promotion\n // codes and read the live net-margin rating before committing to one.\n ADMIN_COUPONS = '/admin/coupons',\n // The panic button (AGL-1501): platform/org/host/user lockdown controls.\n ADMIN_LOCKDOWN = '/admin/lockdown',\n // The narrow lever beside it (AGL-1687): disable or release ONE uploaded\n // file. Its own page rather than a panel on Lockdown because the input is\n // an asset, not a scope — the two forms share no field but the reason.\n ADMIN_MEDIA_QUARANTINE = '/admin/media-quarantine',\n // The INPUT to the two levers above (AGL-1964). Lockdown and quarantine are\n // both things an operator does after learning there is a problem; until\n // this page there was no way for anyone outside the company to say there\n // was one. Reports arrive from the unauthenticated form the tenant runtime\n // serves at /api/report-abuse on every origin.\n ADMIN_ABUSE_REPORTS = '/admin/abuse-reports',\n // The same input, one collection over (AGL-2310). The marketplace's own\n // report button wrote `marketplaceReports` from the day it shipped and\n // nothing read it — every report filed, acknowledged, and unreachable. This\n // is that queue, deliberately shaped like the abuse one rather than as a\n // second vocabulary for the same act.\n // The probes, on a screen (AGL-1900). /api/health/{backups,rate-limits,\n // signups} and /api/admin/email-health each answered a bad-day question to\n // a curl and to nothing else; this is where an operator reads them.\n ADMIN_HEALTH = '/admin/health',\n // The maintenance jobs, on a screen (AGL-1949). audit-archive,\n // reap-plugin-artifacts and reverify-plugin-versions accepted the shared\n // cron secret and nothing else, so the only way to preview or run one was a\n // shell holding the production secret. Separate from Health because Health\n // reads and this acts.\n ADMIN_MAINTENANCE = '/admin/maintenance',\n // The Texas sales tax return (AGL-1900). The figures AGL-1811 computes had\n // only a curl to reach them; filing happens on a fixed quarterly calendar\n // from 2026-09-01, so the return needs a URL a person can bookmark.\n ADMIN_REVENUE = '/admin/revenue',\n // Realised band utilization and the margin that follows from it. Beside\n // Revenue because the two are the halves of one question: that page reports\n // what came in, this one what serving it cost.\n ADMIN_MARGIN_UTILIZATION = '/admin/margin-utilization',\n ADMIN_TAX_RETURN = '/admin/tax-return',\n ADMIN_AUDIT = '/admin/audit',\n ADMIN_USERS = '/admin/users',\n ADMIN_USER_DETAIL = '/admin/users/[uid]',\n ADMIN_FLAGS = '/admin/flags',\n // Platform-wide settings that are not a release flag (AGL-2486). The\n // free-workspace ceiling was the first of them and it opened the\n // Organizations LIST, above the table — a global lever wedged into the\n // screen for browsing individual orgs. Settings that describe the platform\n // rather than one org belong on their own tab, beside the flags.\n ADMIN_SETTINGS = '/admin/settings',\n // Which operator alerts email the operator, immediately or in the daily\n // digest, and whether the channels reach anybody (AGL-3377).\n ADMIN_OPERATOR_ALERTS = '/admin/operator-alerts',\n // One submission or listed plugin in full (AGL-959): the queue index is\n // for scanning, this is where a reviewer reads the manifest, weighs the\n // verifier findings and acts.\n // A page a plugin adds to the staff area (AGL-2939), rendered by the\n // console's generic staff route. The console's own staff routes win the\n // segments they use.\n ADMIN_STAFF_PAGE = '/admin/[staffPage]',\n // Staff support-ticket queue (AGL-849): the operator side of the\n // subscriber `MANAGE_SUPPORT_TICKETS` page — every org's tickets in one place.\n ADMIN_SUPPORT = '/admin/support',\n // Do-not-contact list (AGL-1592): the intake and the queue for Privacy\n // Policy v4 §11's marketing call/text opt-out, and for \"delete the phone\n // number you hold for me\".\n ADMIN_CONTACT_SUPPRESSIONS = '/admin/contact-suppressions',\n ADMIN_EMAILS = '/admin/emails',\n // The one besigner route with no host and no org in it (AGL-749). A system\n // email belongs to the platform, not to a workspace, so it is staff-scoped\n // like the rest of `/admin/*` rather than nested under `[orgSlug]`.\n ADMIN_EMAIL_BESIGNER = '/admin/emails/[templateKey]/versions/[versionId]/besigner',\n ORG_HOME = '/[orgSlug]',\n // Staff build a site for a client on one phone screen (AGL-3958).\n ORG_CLIENT_BUILD = '/[orgSlug]/client-build',\n ORG_MEDIA = '/[orgSlug]/media',\n ORG_DATA = '/[orgSlug]/data',\n /*\n * The organization-level CRM hub (AGL-2630): the same sections the site hub\n * has — `/[orgSlug]/crm/<section>[/<recordId>]` — mounted with an ORG scope,\n * for an org-wide member who works every site's people at once. A bare\n * `/crm` lands on the first section the reader may open, as the site hub's\n * does. What this route adds over the site hub is the cross-site fact the\n * old address book carried — which sites know a person — and what it takes\n * from it is the site every create must name, since a record is always\n * captured BY a site. Served by `ORG_PLUGIN` below, from the CRM's own\n * `orgNavItems`; the constant is the address its links are built from.\n */\n ORG_CRM = '/[orgSlug]/crm',\n /*\n * The generic ORGANIZATION-level plugin route (AGL-2974): every surface an\n * extension declares in `orgNavItems` is served here, at\n * `/[orgSlug]/<href>[/<section>…]`, by\n * `app/(app)/[orgSlug]/[...pluginSlug]/page.tsx`. The org twin of\n * `HOST_PLUGIN`, and like it the one org route whose leaf segment is data:\n * every named org route in this enum wins over it.\n */\n ORG_PLUGIN = '/[orgSlug]/[pluginSlug]',\n ORG_PLUGINS = '/[orgSlug]/plugins',\n // One plugin, as installed in this workspace (AGL-1007): scope, settings\n // and permissions in one place. The segment takes EITHER identifier\n // (AGL-1010) — a listing id for a marketplace install, a registry id for a\n // first-party plugin — hence `pluginRef` rather than `listingId`.\n ORG_PLUGIN_INSTALLATION = '/[orgSlug]/plugins/[pluginRef]',\n // Org-scope marketplace (AGL-772): the single place to browse, view and\n // install marketplace items. The per-site tab it replaced, and the retired\n // `/[orgSlug]/marketplace` seller area, were removed with the rest of the\n // `marketplace` naming in AGL-975 — the word is being freed for a public\n // forum, and a redirect stub sitting on `/marketplace` would have taken the\n // one path that feature wants.\n /*\n * The marketplace's own SECTIONS are not here any more (AGL-3080). Eight\n * of them — browse, licences, upload, the four seller cards and the\n * publish form — were hand-written console routes, and became an\n * `orgNavItems` declaration whose `sections` name those segments once, in\n * the plugin. Restating them here was a second copy of a list the plugin\n * owns, and every entry had zero callers the moment the routes went.\n *\n * What stays is what the CONSOLE still builds: the surface itself, a\n * listing, a publisher, and the installed tab that the org plugins card\n * links across to. A console page linking into a plugin's surface is the\n * ordinary case; a console page enumerating that surface's sections is\n * not.\n */\n ORG_MARKETPLACE = '/[orgSlug]/marketplace',\n /*\n * Marketplace SECTIONS are routes (AGL-2501), for the reason the settings\n * and account sections are: a panel that is not open should cost nothing,\n * and the seller half of this hub reads the organization's REVENUE.\n *\n * The four seller sections are the reason the layout gates rather than the\n * rail. As tabs they were simply not rendered for a member without\n * `publishToMarketplace`; as routes each one is reachable by typing its URL,\n * so the refusal has to sit above them all rather than in the list that\n * draws them.\n *\n * Every segment here is shorter than the ten characters `createResourceUid`\n * emits, so none of them can ever shadow a real `[listingId]` sitting at the\n * same level. `upload` rather than `publish` because `publish/plugin` is\n * already a route beneath this one, and two directories contributing the\n * same segment is a tree Next cannot resolve a layout for.\n */\n ORG_MARKETPLACE_INSTALLED = '/[orgSlug]/marketplace/installed',\n ORG_MARKETPLACE_LISTING = '/[orgSlug]/marketplace/[listingId]',\n ORG_MARKETPLACE_PUBLISHER = '/[orgSlug]/marketplace/publisher/[handle]',\n // Publishing a plugin is a page, not a modal (AGL-1078): the most\n // consequential thing a publisher does now has a URL, so it can be linked,\n // reloaded, and reached from a listing to ship an update.\n ORG_SETTINGS = '/[orgSlug]/settings',\n /*\n * Settings SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening General also mounted the API-keys, SSO and data-export cards and\n * ran every read in them. Routes mount one page and code-split per route.\n * `ORG_SETTINGS` itself redirects to General.\n */\n ORG_SETTINGS_GENERAL = '/[orgSlug]/settings/general',\n ORG_SETTINGS_PROFILE = '/[orgSlug]/settings/profile',\n ORG_SETTINGS_PLUGINS = '/[orgSlug]/settings/plugins',\n ORG_SETTINGS_API_KEYS = '/[orgSlug]/settings/api-keys',\n ORG_SETTINGS_BRANDING = '/[orgSlug]/settings/branding',\n ORG_SETTINGS_SSO = '/[orgSlug]/settings/sso',\n ORG_SETTINGS_OWNERSHIP = '/[orgSlug]/settings/ownership',\n /**\n * Export the people a workspace holds and file a person's erasure\n * (AGL-2839) — outside the CRM, so it is there on every plan.\n */\n ORG_SETTINGS_PRIVACY = '/[orgSlug]/settings/privacy',\n // Holds & reviews (AGL-3368): everything on the workspace that was held,\n // flagged, locked or paused, its status, and the way to request a review.\n // Where every risk notice's \"Request a review\" lands (`?notice=<id>`).\n ORG_SETTINGS_HOLDS = '/[orgSlug]/settings/holds',\n // Import & export (AGL-3535): every resource the workspace can move,\n // workspace packages, and the history of its imports with undo.\n ORG_SETTINGS_DATA = '/[orgSlug]/settings/data',\n ORG_SETTINGS_DELETE = '/[orgSlug]/settings/delete',\n // The `orgSettings` plugin zone's own section (AGL-3711): offered only\n // while a plugin has a card for it.\n ORG_SETTINGS_INTEGRATIONS = '/[orgSlug]/settings/integrations',\n /**\n * Billing, section by section (AGL-2501).\n *\n * `MANAGE_BILLING` is unchanged and is the PLAN section — it resolves to a\n * page inside a route group, which adds no path segment. That is what makes\n * this split free: every link in the console, every hash anchor\n * (`#addons`, `#collaborator-seats`) and Stripe's own dunning mail all point\n * here already and none of them moves. No redirect, and no `?tab=` shim to\n * carry forever.\n */\n MANAGE_BILLING = '/[orgSlug]/billing',\n MANAGE_BILLING_USAGE = '/[orgSlug]/billing/usage',\n MANAGE_BILLING_INVOICES = '/[orgSlug]/billing/invoices',\n MANAGE_BILLING_SETTINGS = '/[orgSlug]/billing/settings',\n // The ONE billing URL that carries no org (AGL-2430).\n //\n // Everything else on this table is org-scoped, which is correct for a\n // console and wrong for the only caller that cannot template a URL:\n // Stripe's dunning emails. \"Payment method updates\" takes a single static\n // link for every customer in the account — there is no `{{org}}` to\n // interpolate — so a table with no org-agnostic billing entry means those\n // emails can only ever point at a page that is not billing. Today all four\n // of them point at the marketing homepage, which is how a failed card\n // becomes a cancelled subscription with no reachable way to fix it.\n //\n // This resolves the org from the SESSION instead of from the URL: one\n // workspace goes straight through, several offer a choice, none says so.\n // `billing` is a reserved subdomain (`RESERVED_SUBDOMAINS`), so no org can\n // hold this slug and the literal segment can never shadow a real\n // `/[orgSlug]`.\n BILLING_ENTRY = '/billing',\n MANAGE_USER_SETTINGS = '/manage/user',\n /*\n * Account SECTIONS are routes (AGL-2501). `HubTabs` mounted every panel —\n * `keepMounted`, with `lazy` off by default and passed by nobody — so\n * opening Account also mounted the email-addresses, passkeys, recent\n * sign-ins, data-export and close-account cards and ran every read in them.\n * Routes mount one page and code-split per route.\n *\n * The segments are the ids the panels carried, because a transactional\n * email already in people's inboxes links `/manage/user?tab=security`:\n * `MANAGE_USER_SETTINGS` forwards each of those ids to the section that\n * holds it, and the segment matching the id is what keeps that map a\n * one-liner rather than a second naming scheme.\n */\n MANAGE_USER_ACCOUNT = '/manage/user/account',\n MANAGE_USER_EMAILS = '/manage/user/emails',\n MANAGE_USER_PROFILE = '/manage/user/profile',\n MANAGE_USER_BASIC = '/manage/user/basic',\n MANAGE_USER_SECURITY = '/manage/user/security',\n MANAGE_USER_CLOSE = '/manage/user/close',\n MANAGE_NOTIFICATIONS = '/manage/notifications',\n /*\n * The preferences, off the feed (AGL-3226).\n *\n * They lived in the header of the feed itself, which was already the\n * tallest thing on that page before per-channel and per-scope settings\n * multiplied them — and it put an edit form above the list it edits, which\n * the console does nowhere else.\n *\n * A CHILD of the feed rather than a sibling under `/manage`, because the\n * relationship is exactly that: it configures what the page above it shows.\n * That also keeps `/manage/notifications` the address in the transactional\n * mail already in people's inboxes, which keeps working and now carries a\n * button to here.\n */\n MANAGE_NOTIFICATION_SETTINGS = '/manage/notifications/settings',\n /*\n * A URL for the defect channel (AGL-2486). \"Report an issue\" was reachable\n * only by opening the account menu, so nothing outside the console could\n * send anyone to it — the repo's README pointed at GitHub issues, which we\n * do not use, and every other \"tell us about it\" surface had the same\n * problem. User-level rather than org-scoped because the dialog is: it\n * needs no workspace, no role and no paid plan.\n */\n MANAGE_REPORT_ISSUE = '/manage/report-issue',\n AUTH_SIGN_IN = '/signin',\n AUTH_SIGN_OUT = '/signout',\n AUTH_SIGN_UP = '/signup',\n AUTH_VERIFY_EMAIL = '/verify-email',\n HOST_LIST = '/[orgSlug]/hosts',\n HOST_CONTENT = '/[orgSlug]/hosts/[host]/content',\n // The collection is a PATH SEGMENT, not `?collection=` (AGL-2498).\n //\n // It was a query parameter because the manager began as one page with a\n // Select on it, and the Select was the only thing that knew which\n // collection you were looking at. That is the wrong owner: which collection\n // is open is the page's IDENTITY, not a filter applied to it. As a\n // parameter it could be dropped by any link that rebuilt the query, it\n // sorted below `?tab=` in a URL nobody could read, and — the reason this\n // moved — it left the entry beneath it with nowhere to hang: an entry is\n // addressed `collection + entry`, and half of that address cannot live in\n // the path while the other half lives in the query.\n //\n // The segment is the collection's SLUG, not its document id, and that is a\n // correction rather than a preference. Collection document ids are not\n // uniform — the seeded ones were given readable ids (`blog`, `changelog`)\n // and everything created since gets a `createResourceUid`, so routing by id\n // produced `/content/changelog` beside `/content/QgXv7lU_rG` on ONE site.\n //\n // A slug is the right key on the merits too: it is unique per host and kind\n // (claimed in a transaction by /api/hosts/collections), it is the\n // collection's own public address, and it is the thing an author recognises.\n // A document id is still ACCEPTED and rewritten to the slug form, so no\n // existing link breaks.\n //\n // `?collection=` still resolves the same way (AGL-845's DAM deep links are\n // in the wild).\n HOST_CONTENT_COLLECTION = '/[orgSlug]/hosts/[host]/content/[collectionSlug]',\n // One entry's detail page (AGL-2498). `entries` is a LITERAL segment rather\n // than `content/[collectionId]/[entryId]`, and it is load-bearing twice\n // over: it mirrors the Firestore path the entry actually lives at\n // (`collections/{c}/entries/{e}`), and it keeps the id space clear so a\n // later sibling — a collection's own settings page, say — cannot be\n // shadowed by an entry that happens to be called the same thing.\n //\n // `[entryId]` also carries the sentinel `new`, which is a draft that has no\n // document yet. Entry ids come from `createResourceUid`, so nothing real\n // can collide with it.\n CONTENT_ENTRY_DETAILS = '/[orgSlug]/hosts/[host]/content/[collectionSlug]/entries/[entryId]',\n MANAGE_TEAM = '/[orgSlug]/team',\n /*\n * Team SECTIONS are routes, not tabs (AGL-2501). A section's bundle then\n * arrives when a reader opens it and never before, the section is linkable,\n * and the active state is a fact about the URL rather than state that has to\n * be kept in sync with it. `MANAGE_TEAM` itself redirects to members.\n */\n MANAGE_TEAM_MEMBERS = '/[orgSlug]/team/members',\n MANAGE_TEAM_ROLES = '/[orgSlug]/team/roles',\n MANAGE_TEAM_ACTIVITY = '/[orgSlug]/team/activity',\n MANAGE_TEAM_MEMBER = '/[orgSlug]/team/[uid]',\n // Support is an UMBRELLA, not a page (AGL-1158). The two channels beneath\n // it are separate features that were only ever one screen: they are gated\n // differently (tickets need a first-response commitment, from Pro; the\n // forum is open to every tier including Free) and they fail independently.\n // Sharing one route made them share a blast radius — AGL-1157 was one bad\n // line in the common loader that emptied BOTH lists at once.\n //\n // `MANAGE_SUPPORT` itself stays, and stays the nav tab's href: it now\n // forwards to whichever channel the org's tier makes primary, so a\n // forum-only workspace lands on a whole forum instead of a half-empty page\n // beside a ticket card it may not use.\n MANAGE_SUPPORT = '/[orgSlug]/support',\n MANAGE_SUPPORT_TICKETS = '/[orgSlug]/support/tickets',\n MANAGE_SUPPORT_FORUM = '/[orgSlug]/support/forum',\n /*\n * The ONE support URL that carries no org (AGL-3265), and the second member\n * of the apex category `BILLING_ENTRY` opened.\n *\n * Everything a person is handed when they need help — a receipt, a system\n * email footer, a Stripe customer portal, a self-host runbook — stores ONE\n * link for every customer, with no workspace to interpolate. `/[orgSlug]`\n * cannot be templated into any of them, so the only support address those\n * surfaces could carry was one outside the console entirely.\n *\n * This resolves the workspace from the SESSION instead: one workspace goes\n * straight through, several offer a choice, none says so. It forwards to\n * `MANAGE_SUPPORT`, which is itself an umbrella — so the tier decision about\n * tickets versus the forum stays in the one place that already makes it.\n *\n * `support` is a reserved subdomain (`RESERVED_SUBDOMAINS` in\n * `host-naming.ts`), so no org can hold this slug and the literal segment\n * can never shadow a real `/[orgSlug]`. That is the same guarantee\n * `BILLING_ENTRY` rests on, and it is a precondition rather than a\n * coincidence: an apex route whose segment an org could claim would take a\n * workspace's own address away from it.\n */\n SUPPORT_ENTRY = '/support',\n HOST_DASHBOARD = '/[orgSlug]/hosts/[host]',\n // The catch-all console page a plugin's own nav items resolve against\n // (`app/(app)/[orgSlug]/hosts/[host]/[pluginSlug]/page.tsx`). Plugin slugs\n // are open-ended, so this is the one route whose leaf segment is data.\n HOST_PLUGIN = '/[orgSlug]/hosts/[host]/[pluginSlug]',\n HOST_INBOX = '/[orgSlug]/hosts/[host]/inbox',\n HOST_MEDIA = '/[orgSlug]/hosts/[host]/media',\n HOST_SETUP = '/[orgSlug]/hosts/[host]/setup',\n /*\n * Setup SECTIONS are routes (AGL-2501), so an unopened one costs neither a\n * read nor a byte. `HOST_SETUP` stays the nav tab's href and redirects to\n * Basic details, honouring the `?tab=` ids these sections were deep-linked\n * by — unlike the settings and marketplace hubs, links holding those ids are\n * demonstrably in the wild and two are built in this repo.\n */\n HOST_SETUP_DETAILS = '/[orgSlug]/hosts/[host]/setup/details',\n HOST_SETUP_SEO = '/[orgSlug]/hosts/[host]/setup/seo',\n HOST_SETUP_TRACKING = '/[orgSlug]/hosts/[host]/setup/tracking',\n HOST_SETUP_THEME = '/[orgSlug]/hosts/[host]/setup/theme',\n HOST_SETUP_BUSINESS = '/[orgSlug]/hosts/[host]/setup/business',\n HOST_SETUP_EMAILS = '/[orgSlug]/hosts/[host]/setup/emails',\n // The `hostSettings` plugin zone's own section (AGL-3711): offered only\n // while a plugin has a card for it.\n HOST_SETUP_INTEGRATIONS = '/[orgSlug]/hosts/[host]/setup/integrations',\n // Host Admin area (AGL-1014): owner/admin-only controls — per-site plugin\n // enablement and the Danger zone — out of the Setup page collaborators\n // legitimately visit.\n HOST_ADMIN = '/[orgSlug]/hosts/[host]/admin',\n /*\n * Site-admin SECTIONS are routes (AGL-2501), so an unopened one costs neither\n * a read nor a byte. `HOST_ADMIN` redirects to Plugins and still honours the\n * `?tab=` ids these sections were deep-linked by.\n */\n /*\n * The site as an OBJECT: its name in the console and the subdomain it\n * answers to. First in the rail because an address and a name are what\n * identify the thing every other section governs.\n */\n HOST_ADMIN_GENERAL = '/[orgSlug]/hosts/[host]/admin/general',\n HOST_ADMIN_PLUGINS = '/[orgSlug]/hosts/[host]/admin/plugins',\n /*\n * One plugin, as installed on ONE site (AGL-428, AGL-1014) — the\n * site-scoped twin of `ORG_PLUGIN_INSTALLATION`, reached by clicking a row\n * on the site's plugin\n * list exactly as the workspace list opens its own detail page. The segment\n * is `[pluginRef]` and accepts either identifier for the same reason the\n * workspace route does (AGL-1010): a marketplace install is keyed by listing\n * id, a first-party plugin by its registry id, and an admin does not think\n * of the two as different things.\n */\n HOST_ADMIN_PLUGIN = '/[orgSlug]/hosts/[host]/admin/plugins/[pluginRef]',\n HOST_ADMIN_DOMAIN = '/[orgSlug]/hosts/[host]/admin/domain',\n HOST_ADMIN_SECURITY = '/[orgSlug]/hosts/[host]/admin/security',\n /*\n * The designed screen behind each status code, and the switch that shows the\n * 503 one at every address at once (AGL-3178).\n *\n * Beside Security because both answer what a visitor's browser is handed on\n * a page the site did not design for them, and above Activity because it is\n * a control rather than a record.\n */\n HOST_ADMIN_ERROR_PAGES = '/[orgSlug]/hosts/[host]/admin/error-pages',\n HOST_ADMIN_ACTIVITY = '/[orgSlug]/hosts/[host]/admin/activity',\n /*\n * Backup, restore and publishing the site as a template — moving the whole\n * site somewhere else, or bringing it back. Its own section rather than the\n * Danger zone: restore overwrites, but export and publishing a template do\n * not, and filing all three under a destructive heading mislabels two of\n * them.\n */\n HOST_ADMIN_BACKUP = '/[orgSlug]/hosts/[host]/admin/backup',\n HOST_ADMIN_DANGER = '/[orgSlug]/hosts/[host]/admin/danger',\n HOST_THEME = '/[orgSlug]/hosts/[host]/theme',\n /*\n * The automation section: Workflows, Actions and Webhooks under one parent.\n * The parent is named for what the three have in common, so no tab shares a\n * name with the section that contains it.\n */\n HOST_AUTOMATION = '/[orgSlug]/hosts/[host]/automation',\n HOST_DATA = '/[orgSlug]/hosts/[host]/data',\n HOST_LOGIC = '/[orgSlug]/hosts/[host]/logic',\n HOST_COMPONENTS = '/[orgSlug]/hosts/[host]/components',\n // Component detail (AGL-693): the listing links here, and the besigner is\n // reached from here — matching SCREEN_DETAILS rather than jumping a row\n // straight into the editor.\n COMPONENT_DETAILS = '/[orgSlug]/hosts/[host]/components/[componentId]',\n HOST_FORMS = '/[orgSlug]/hosts/[host]/forms',\n // A form is a designable document, so its surface is the component one:\n // a list, a detail page that owns the versions, and the besigner reached\n // from there. Deliberately NOT a row that jumps into the editor — a form's\n // routing and consent field are edited on the detail page, and those decide\n // what the design is then checked against.\n FORM_DETAILS = '/[orgSlug]/hosts/[host]/forms/[formId]',\n HOST_TEMPLATES = '/[orgSlug]/hosts/[host]/templates',\n // Template detail (AGL-694), the counterpart to COMPONENT_DETAILS. Note\n // TEMPLATE_BESIGNER carries no versionId — templates version but never\n // publish, so there is no \"current\" pointer to route through.\n TEMPLATE_DETAILS = '/[orgSlug]/hosts/[host]/templates/[templateId]',\n HOST_MARKETING = '/[orgSlug]/hosts/[host]/marketing',\n // Events now come from the events-calendar plugin, served by the generic\n // `[orgSlug]/hosts/[host]/[pluginSlug]` route (AGL-394).\n HOST_REDIRECTS = '/[orgSlug]/hosts/[host]/redirects',\n HOST_USERS = '/[orgSlug]/hosts/[host]/users',\n HOST_ANALYTICS = '/[orgSlug]/hosts/[host]/analytics',\n COMPONENT_BESIGNER = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/besigner',\n /*\n * A document a plugin keeps under a site and authors in the besigner\n * (`plugin-manager/besigner-documents.ts`): the segment is the kind the\n * plugin declared, the details page is the plugin's own at\n * `<segment>/<docId>`, and the editor and preview are the console's.\n */\n PLUGIN_DOCUMENT_DETAILS = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]',\n PLUGIN_DOCUMENT_BESIGNER = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/besigner',\n TEMPLATE_BESIGNER = '/[orgSlug]/hosts/[host]/templates/[templateId]/besigner',\n LAYOUT_BESIGNER = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/besigner',\n // Draft preview for every besigner document kind (AGL-1203). Screens had\n // the only preview route; components, layouts and templates showed a\n // Preview button that did nothing. Each renders the same localStorage\n // snapshot through the site renderer, so they work on localhost with no\n // deployment. TEMPLATE_PREVIEW carries no versionId, matching\n // TEMPLATE_BESIGNER.\n COMPONENT_PREVIEW = '/[orgSlug]/hosts/[host]/components/[componentId]/versions/[versionId]/preview',\n PLUGIN_DOCUMENT_PREVIEW = '/[orgSlug]/hosts/[host]/[documentSegment]/[docId]/versions/[versionId]/preview',\n TEMPLATE_PREVIEW = '/[orgSlug]/hosts/[host]/templates/[templateId]/preview',\n LAYOUT_PREVIEW = '/[orgSlug]/hosts/[host]/layouts/[layoutId]/versions/[versionId]/preview',\n // The list sits at the bare path, like HOST_COMPONENTS. It used to be\n // `/layouts/list`: the pages router had a `layouts/index.tsx` that\n // redirected to `layouts/list.tsx`, and the App Router migration carried\n // the pair over verbatim. Components, added later, had no such legacy and\n // put its list at the bare path — which is the shape we want everywhere.\n // `/layouts/list` still serves the same page so old links survive — an\n // alias, not a redirect, because the bare path used to answer a cached 308\n // pointing at `/list` and redirecting back would loop. See the comment in\n // `app/(app)/[orgSlug]/hosts/[host]/screens/list/page.tsx`.\n HOST_LAYOUTS = '/[orgSlug]/hosts/[host]/layouts',\n // Layout detail (AGL-695), completing the list → detail → besigner shape\n // across screens, components, templates and layouts.\n LAYOUT_DETAILS = '/[orgSlug]/hosts/[host]/layouts/[layoutId]',\n SCREEN_BESIGNER = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/besigner',\n // Per-site email besigner (AGL-770): a site owner designs a transactional\n // email their site sends. The host-scoped sibling of the staff system-email\n // editor (ADMIN_EMAIL_BESIGNER).\n HOST_EMAIL_BESIGNER = '/[orgSlug]/hosts/[host]/emails/[templateKey]/versions/[versionId]/besigner',\n SCREEN_DETAILS = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/view',\n SCREEN_PREVIEW = '/[orgSlug]/hosts/[host]/screens/[screenId]/versions/[versionId]/preview',\n // Bare path, matching HOST_LAYOUTS and HOST_COMPONENTS — see the note on\n // HOST_LAYOUTS. `/screens/list` still serves the same page as an alias.\n HOST_SCREENS = '/[orgSlug]/hosts/[host]/screens',\n}\n\n/**\n * Params required to build each {@link Route}.\n *\n * Deliberately NOT extended from an index signature (AGL-685). It used to be\n * `extends Record<keyof any, any>`, which meant any `Route` missing an entry\n * here silently typed its payload as `any` — `Route.HOST_SETUP` was one of\n * them, and `buildRoute(Route.HOST_SETUP, { hostId })` compiled cleanly while\n * emitting `/<orgSlug?>/hosts/<host?>/setup`. Without the index signature a\n * new `Route` with no entry is a compile error, which is the whole point of\n * the payload being required in the first place.\n */\nexport interface RoutePayload {\n [Route.AUTH_SIGN_UP]: undefined\n [Route.AUTH_SIGN_IN]: undefined\n [Route.AUTH_SIGN_OUT]: undefined\n [Route.AUTH_VERIFY_EMAIL]: undefined\n [Route.SCREEN_BESIGNER]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.ORG_HOME]: { orgSlug: string }\n [Route.ORG_CLIENT_BUILD]: { orgSlug: string }\n [Route.HOST_DASHBOARD]: { orgSlug: string; host: string }\n [Route.ADMIN_ORGS]: undefined\n [Route.ADMIN_ORG_DETAIL]: { orgId: string }\n [Route.ADMIN_ORG_HOST_DETAIL]: { orgId: string; hostId: string }\n [Route.ADMIN_SITES]: undefined\n [Route.ADMIN_SITE_DETAIL]: { hostId: string }\n [Route.ADMIN_SITE_PREVIEW]: { hostId: string; kind: string; docId: string }\n [Route.ADMIN_OVERVIEW]: undefined\n [Route.ADMIN_COUPONS]: undefined\n [Route.ADMIN_LOCKDOWN]: undefined\n [Route.ADMIN_MEDIA_QUARANTINE]: undefined\n [Route.ADMIN_ABUSE_REPORTS]: undefined\n [Route.ADMIN_HEALTH]: undefined\n [Route.ADMIN_MAINTENANCE]: undefined\n [Route.ADMIN_REVENUE]: undefined\n [Route.ADMIN_MARGIN_UTILIZATION]: undefined\n [Route.ADMIN_TAX_RETURN]: undefined\n [Route.ADMIN_AUDIT]: undefined\n [Route.ADMIN_USERS]: undefined\n [Route.ADMIN_USER_DETAIL]: { uid: string }\n [Route.ADMIN_FLAGS]: undefined\n [Route.ADMIN_SETTINGS]: undefined\n [Route.ADMIN_OPERATOR_ALERTS]: undefined\n [Route.ADMIN_STAFF_PAGE]: { staffPage: string }\n [Route.ADMIN_SUPPORT]: undefined\n [Route.ADMIN_CONTACT_SUPPRESSIONS]: undefined\n [Route.ADMIN_EMAILS]: undefined\n [Route.ADMIN_EMAIL_BESIGNER]: { templateKey: string; versionId: string }\n [Route.HOST_EMAIL_BESIGNER]: {\n orgSlug: string\n host: string\n templateKey: string\n versionId: string\n }\n [Route.ORG_MEDIA]: { orgSlug: string }\n [Route.ORG_DATA]: { orgSlug: string }\n [Route.ORG_CRM]: { orgSlug: string }\n [Route.ORG_PLUGIN]: { orgSlug: string; pluginSlug: string }\n [Route.ORG_PLUGINS]: { orgSlug: string }\n [Route.ORG_PLUGIN_INSTALLATION]: { orgSlug: string; pluginRef: string }\n [Route.ORG_MARKETPLACE]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_INSTALLED]: { orgSlug: string }\n [Route.ORG_MARKETPLACE_LISTING]: { orgSlug: string; listingId: string }\n [Route.ORG_MARKETPLACE_PUBLISHER]: { orgSlug: string; handle: string }\n [Route.MANAGE_USER_SETTINGS]: undefined\n [Route.MANAGE_USER_ACCOUNT]: undefined\n [Route.MANAGE_USER_EMAILS]: undefined\n [Route.MANAGE_USER_PROFILE]: undefined\n [Route.MANAGE_USER_BASIC]: undefined\n [Route.MANAGE_USER_SECURITY]: undefined\n [Route.MANAGE_USER_CLOSE]: undefined\n [Route.MANAGE_NOTIFICATIONS]: undefined\n [Route.MANAGE_NOTIFICATION_SETTINGS]: undefined\n [Route.MANAGE_REPORT_ISSUE]: undefined\n [Route.ORG_SETTINGS]: { orgSlug: string }\n [Route.ORG_SETTINGS_GENERAL]: { orgSlug: string }\n [Route.ORG_SETTINGS_PROFILE]: { orgSlug: string }\n [Route.ORG_SETTINGS_PLUGINS]: { orgSlug: string }\n [Route.ORG_SETTINGS_API_KEYS]: { orgSlug: string }\n [Route.ORG_SETTINGS_BRANDING]: { orgSlug: string }\n [Route.ORG_SETTINGS_SSO]: { orgSlug: string }\n [Route.ORG_SETTINGS_OWNERSHIP]: { orgSlug: string }\n [Route.ORG_SETTINGS_PRIVACY]: { orgSlug: string }\n [Route.ORG_SETTINGS_HOLDS]: { orgSlug: string }\n [Route.ORG_SETTINGS_DATA]: { orgSlug: string }\n [Route.ORG_SETTINGS_DELETE]: { orgSlug: string }\n [Route.ORG_SETTINGS_INTEGRATIONS]: { orgSlug: string }\n [Route.HOST_LIST]: { orgSlug: string }\n [Route.HOST_CONTENT]: { orgSlug: string; host: string }\n [Route.HOST_CONTENT_COLLECTION]: {\n orgSlug: string\n host: string\n /** The collection's slug — see the route. A document id also resolves. */\n collectionSlug: string\n }\n [Route.CONTENT_ENTRY_DETAILS]: {\n orgSlug: string\n host: string\n collectionSlug: string\n /**\n * An entry id, or the literal `new` for a draft with no document yet.\n *\n * An ID here while the collection above is a SLUG, deliberately. An entry\n * slug is unique only WITHIN its collection and it is editable on the\n * detail page — so routing by it would move the console address out from\n * under someone in the middle of typing in the slug field, and a draft has\n * no slug at all to route by. The entry id is also what every activity\n * record and the DAM's \"Used on\" list already carry.\n */\n entryId: string\n }\n [Route.MANAGE_TEAM]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBERS]: { orgSlug: string }\n [Route.MANAGE_TEAM_ROLES]: { orgSlug: string }\n [Route.MANAGE_TEAM_ACTIVITY]: { orgSlug: string }\n [Route.MANAGE_TEAM_MEMBER]: { orgSlug: string; uid: string }\n [Route.MANAGE_SUPPORT]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_TICKETS]: { orgSlug: string }\n [Route.MANAGE_SUPPORT_FORUM]: { orgSlug: string }\n [Route.MANAGE_BILLING]: { orgSlug: string }\n [Route.MANAGE_BILLING_USAGE]: { orgSlug: string }\n [Route.MANAGE_BILLING_INVOICES]: { orgSlug: string }\n [Route.MANAGE_BILLING_SETTINGS]: { orgSlug: string }\n [Route.BILLING_ENTRY]: undefined\n [Route.SUPPORT_ENTRY]: undefined\n [Route.HOST_INBOX]: { orgSlug: string; host: string }\n [Route.HOST_SETUP]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_DETAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_SEO]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_TRACKING]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_THEME]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_BUSINESS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_EMAILS]: { orgSlug: string; host: string }\n [Route.HOST_SETUP_INTEGRATIONS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_GENERAL]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGINS]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_PLUGIN]: { orgSlug: string; host: string; pluginRef: string }\n [Route.HOST_ADMIN_DOMAIN]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_SECURITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ERROR_PAGES]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_ACTIVITY]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_BACKUP]: { orgSlug: string; host: string }\n [Route.HOST_ADMIN_DANGER]: { orgSlug: string; host: string }\n [Route.HOST_PLUGIN]: { orgSlug: string; host: string; pluginSlug: string }\n [Route.HOST_MEDIA]: { orgSlug: string; host: string }\n [Route.HOST_THEME]: { orgSlug: string; host: string }\n [Route.HOST_AUTOMATION]: { orgSlug: string; host: string }\n [Route.HOST_DATA]: { orgSlug: string; host: string }\n [Route.HOST_LOGIC]: { orgSlug: string; host: string }\n [Route.HOST_COMPONENTS]: { orgSlug: string; host: string }\n [Route.COMPONENT_DETAILS]: {\n orgSlug: string\n host: string\n componentId: string\n }\n [Route.HOST_FORMS]: { orgSlug: string; host: string }\n [Route.FORM_DETAILS]: {\n orgSlug: string\n host: string\n formId: string\n }\n [Route.HOST_TEMPLATES]: { orgSlug: string; host: string }\n [Route.TEMPLATE_DETAILS]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.HOST_MARKETING]: { orgSlug: string; host: string }\n [Route.HOST_REDIRECTS]: { orgSlug: string; host: string }\n [Route.HOST_USERS]: { orgSlug: string; host: string }\n [Route.HOST_ANALYTICS]: { orgSlug: string; host: string }\n [Route.COMPONENT_BESIGNER]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_DETAILS]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n }\n [Route.PLUGIN_DOCUMENT_BESIGNER]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_BESIGNER]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_BESIGNER]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.COMPONENT_PREVIEW]: {\n orgSlug: string\n host: string\n componentId: string\n versionId: string\n }\n [Route.PLUGIN_DOCUMENT_PREVIEW]: {\n orgSlug: string\n host: string\n documentSegment: string\n docId: string\n versionId: string\n }\n [Route.TEMPLATE_PREVIEW]: {\n orgSlug: string\n host: string\n templateId: string\n }\n [Route.LAYOUT_PREVIEW]: {\n orgSlug: string\n host: string\n layoutId: string\n versionId: string\n }\n [Route.HOST_LAYOUTS]: { orgSlug: string; host: string }\n [Route.LAYOUT_DETAILS]: { orgSlug: string; host: string; layoutId: string }\n [Route.SCREEN_DETAILS]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.SCREEN_PREVIEW]: {\n orgSlug: string\n host: string\n screenId: string\n versionId: string\n }\n [Route.HOST_SCREENS]: { orgSlug: string; host: string }\n}\n\nexport const routeReplacePattern = /\\[([^\\]]+)\\]/g\n\n/**\n * Builds a concrete path from a {@link Route} template. The payload is\n * REQUIRED whenever the route declares params (e.g. `orgSlug`, `hostId`) and\n * omitted only for param-less routes — so a forgotten `orgSlug` is a compile\n * error, not a `/<orgSlug?>/…` link that breaks at runtime (AGL-621).\n */\nexport function buildRoute<Tmpl extends Route>(\n template: Tmpl,\n ...[payload]: RoutePayload[Tmpl] extends undefined\n ? [payload?: undefined]\n : [payload: RoutePayload[Tmpl]]\n) {\n return template.replace(routeReplacePattern, (match, key) => {\n const value = (payload as Record<string, unknown> | undefined)?.[key]\n return value != null ? String(value) : `<${key}?>`\n })\n}\n"],"names":["Route","routeReplacePattern","buildRoute","template","payload","replace","match","key","value","String"],"mappings":"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"}
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
'#from-a-brief': 'Open Components for the site and choose Create with AI, beside Templates and Create Component.'
|
|
64
64
|
},
|
|
65
65
|
aiCreate: {
|
|
66
|
-
'#without-the-add-on': 'On
|
|
66
|
+
'#without-the-add-on': 'On every paid plan, Create with AI runs on the AI credits your plan includes each month; nothing else needs to be bought.'
|
|
67
67
|
},
|
|
68
68
|
aiCredits: {
|
|
69
69
|
'#before-a-job-starts': 'Every job tells you what it will cost before it spends anything: A plan shows Estimated cost: About N credits (up to M) beside Confirm plan: about what jobs like it cost on real builds, and the most it can cost.',
|
|
70
70
|
'#past-the-band': 'On a paid plan, AI keeps working past the included band, and the extra credits are billed on your monthly invoice at your plan\'s rate per 1,000 credits.',
|
|
71
|
-
'#see-your-credits': 'Billing → Usage → AI credits shows the credits drawn this month against the
|
|
71
|
+
'#see-your-credits': 'Billing → Usage → AI credits shows the credits drawn this month against the pool, and how many of them your tier adds.'
|
|
72
72
|
},
|
|
73
73
|
aiCrm: {
|
|
74
74
|
'#draft-an-email': 'In the email composer on a contact, a deal or a lead, describe what the email should say (for example, "follow up on the quote and offer a call next week") and press Draft the message.',
|
|
@@ -136,10 +136,10 @@
|
|
|
136
136
|
},
|
|
137
137
|
aiStart: {
|
|
138
138
|
'#audience': 'Who is it for? is optional. It is who every page and every search listing is written for, and it decides which questions the contact form asks.',
|
|
139
|
-
'#details': '
|
|
139
|
+
'#details': 'Business phone and Business email come first. Both are optional.',
|
|
140
140
|
'#how-do-you-want-to-start': 'The first step asks for the site\'s Site name and Subdomain, its free address on Aglyn.',
|
|
141
|
-
'#review': 'The last step lists every answer: the kind of site, who it is for, the style, where form submissions go, the pages and, on a paid plan, the welcome email
|
|
142
|
-
'#the-sites-search-listing': 'The site\'s own search title and description
|
|
141
|
+
'#review': 'The last step lists every answer: the kind of site, who it is for, the style, where form submissions go, the pages and, on a paid plan, the welcome email, then every detail you gave, such as the phone, the email, the…',
|
|
142
|
+
'#the-sites-search-listing': 'The site\'s own search title and description are the fallback for every page without its own, and what search results and AI assistants read about the site as a whole.',
|
|
143
143
|
'#your-business': 'What kind of site are you creating? is the one required answer.'
|
|
144
144
|
},
|
|
145
145
|
aiTemplate: {
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
'#crm-records': 'Your CRM — the contacts captured from forms, member sign-ups, buyers and bookings, and the companies and deals your team files beside them — is priced as one CRM records band, not a hard cap.',
|
|
170
170
|
'#current-plan': 'The Current plan card at the top of the Plan tab names your plan, the state of its subscription and its price, with the plan\'s headline limits — sites, team seats and campaign emails a month — beside it.',
|
|
171
171
|
'#if-you-would-rather-uploads-stopped': 'Set a monthly storage cap on the Storage cap card under Billing → Usage when you would rather new uploads stopped than pay for extra storage past a figure you choose.',
|
|
172
|
+
'#locked-features': 'A feature your plan doesn\'t include stays on screen, turned off, with Included from and the plan that has it, and an Upgrade link to the plans on this page; moving to that plan turns it on, with nothing else to set up.',
|
|
172
173
|
'#made-with-badge': 'Published pages on the Free plan show a small Made with Aglyn badge; plans that include badge removal drop it automatically, with nothing to switch on.',
|
|
173
174
|
'#metered-usage-estimate': 'The Metered usage estimate card prices this month\'s usage so far, using the same arithmetic as the invoice: only what goes past your plan\'s included amount is charged, at our cost plus 30% after card fees.',
|
|
174
175
|
'#one-to-one-email': 'A one-to-one email is a message a teammate writes to one person from a CRM record — Send email on a contact\'s, a lead\'s or a deal\'s page, described under Activities & the timeline — as against a campaign, which is…',
|
|
@@ -592,6 +593,7 @@
|
|
|
592
593
|
'#audit-log': 'A record of staff actions, newest first. Click an entry to read its reason, note and before/after below the list.',
|
|
593
594
|
'#billing-insight': 'Every organization\'s Stripe invoice history and default payment method (with delinquency state) render on its detail page.',
|
|
594
595
|
'#broadcast-announcements': 'Push a product announcement or maintenance notice as an in-app notification to every organization\'s owner/admins (optionally one plan tier), respecting each recipient\'s mute preferences; audited.',
|
|
596
|
+
'#bundle-link': 'The Bundle checkout link card on an organization\'s page, and the header of the Hand off dialog, make a link to the owner\'s Billing page with the plan, add-ons, billing interval and coupon already chosen.',
|
|
595
597
|
'#contact-suppressions': 'The platform do-not-contact list for phone numbers — calls and texts.',
|
|
596
598
|
'#coupons': 'Discount codes for Aglyn\'s own subscriptions. They live in Stripe — the console creates them there and reads them back, so a coupon made in the Stripe Dashboard shows up here and vice versa.',
|
|
597
599
|
'#create-a-coupon': 'Make a discount code for Aglyn\'s own subscriptions here: give it a name, a type and a duration, read the cost warnings the form shows, then create it.',
|