@crvouga/mockingbird-notifications 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -2
- package/dist/chunk-GRGR3QZM.js +1066 -0
- package/dist/chunk-GRGR3QZM.js.map +7 -0
- package/dist/chunk-JQ6KPT2C.js +202 -0
- package/dist/chunk-JQ6KPT2C.js.map +7 -0
- package/dist/chunk-LM7R7AR7.js +183 -0
- package/dist/chunk-LM7R7AR7.js.map +7 -0
- package/dist/chunk-QE3TRUI6.js +444 -0
- package/dist/chunk-QE3TRUI6.js.map +7 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +411 -0
- package/dist/index.js +95 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +177 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +7 -0
- package/dist/ui.d.ts +445 -0
- package/dist/ui.js +1923 -0
- package/dist/ui.js.map +7 -0
- package/package.json +88 -4
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PAGE_STYLES,
|
|
3
|
+
STYLES
|
|
4
|
+
} from "./chunk-QE3TRUI6.js";
|
|
5
|
+
|
|
6
|
+
// src/model.ts
|
|
7
|
+
var SERVICE_KINDS = [
|
|
8
|
+
"twilio",
|
|
9
|
+
"resend",
|
|
10
|
+
"fcm",
|
|
11
|
+
"ses",
|
|
12
|
+
"sns",
|
|
13
|
+
"customerio",
|
|
14
|
+
"mailosaur"
|
|
15
|
+
];
|
|
16
|
+
var SERVICE_LABELS = {
|
|
17
|
+
twilio: "Twilio",
|
|
18
|
+
resend: "Resend",
|
|
19
|
+
fcm: "Firebase Cloud Messaging",
|
|
20
|
+
ses: "Amazon SES",
|
|
21
|
+
sns: "Amazon SNS",
|
|
22
|
+
customerio: "Customer.io",
|
|
23
|
+
mailosaur: "Mailosaur"
|
|
24
|
+
};
|
|
25
|
+
var toMs = (value) => typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
26
|
+
var searchable = (n) => [
|
|
27
|
+
n.to,
|
|
28
|
+
n.from.address,
|
|
29
|
+
n.from.name ?? "",
|
|
30
|
+
n.preview,
|
|
31
|
+
n.sms?.body ?? "",
|
|
32
|
+
n.email?.subject ?? "",
|
|
33
|
+
n.email?.text ?? "",
|
|
34
|
+
n.push?.title ?? "",
|
|
35
|
+
n.push?.body ?? ""
|
|
36
|
+
].join("\n").toLowerCase();
|
|
37
|
+
var matches = (n, filter) => {
|
|
38
|
+
if (filter.channel && n.channel !== filter.channel) return false;
|
|
39
|
+
if (filter.service && n.service !== filter.service) return false;
|
|
40
|
+
if (filter.source && n.source !== filter.source) return false;
|
|
41
|
+
if (filter.namespace && n.namespace !== filter.namespace) return false;
|
|
42
|
+
if (filter.to && n.to.toLowerCase() !== filter.to.toLowerCase()) return false;
|
|
43
|
+
if (filter.contains && !searchable(n).includes(filter.contains.toLowerCase())) return false;
|
|
44
|
+
if (filter.since !== void 0 && Date.parse(n.createdAt) < toMs(filter.since)) return false;
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
var filterFromSearch = (params) => {
|
|
48
|
+
const filter = {};
|
|
49
|
+
const channel = params.get("channel");
|
|
50
|
+
if (channel) {
|
|
51
|
+
if (channel !== "sms" && channel !== "email" && channel !== "push")
|
|
52
|
+
throw new Error(`channel: expected sms, email or push, got ${JSON.stringify(channel)}`);
|
|
53
|
+
filter.channel = channel;
|
|
54
|
+
}
|
|
55
|
+
const service = params.get("service");
|
|
56
|
+
if (service) {
|
|
57
|
+
if (!SERVICE_KINDS.includes(service))
|
|
58
|
+
throw new Error(`service: expected one of ${SERVICE_KINDS.join(", ")}`);
|
|
59
|
+
filter.service = service;
|
|
60
|
+
}
|
|
61
|
+
for (const key of ["source", "namespace", "to", "contains"]) {
|
|
62
|
+
const value = params.get(key);
|
|
63
|
+
if (value) filter[key] = value;
|
|
64
|
+
}
|
|
65
|
+
const since = params.get("since");
|
|
66
|
+
if (since) {
|
|
67
|
+
if (Number.isNaN(toMs(since))) throw new Error("since: expected ISO-8601 or epoch milliseconds");
|
|
68
|
+
filter.since = since;
|
|
69
|
+
}
|
|
70
|
+
return filter;
|
|
71
|
+
};
|
|
72
|
+
var filterToSearch = (filter) => {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
for (const [key, value] of Object.entries(filter))
|
|
75
|
+
if (value !== void 0 && value !== "") params.set(key, String(value));
|
|
76
|
+
return params;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/mime.ts
|
|
80
|
+
var utf8 = new TextDecoder();
|
|
81
|
+
var base64Bytes = (value) => {
|
|
82
|
+
const binary = atob(value.replace(/[^A-Za-z0-9+/=]/g, ""));
|
|
83
|
+
const bytes = new Uint8Array(binary.length);
|
|
84
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
85
|
+
return bytes;
|
|
86
|
+
};
|
|
87
|
+
var quotedPrintableBytes = (value, underscoreSpace = false) => {
|
|
88
|
+
const source = value.replace(/=\r?\n/g, "");
|
|
89
|
+
const out = [];
|
|
90
|
+
for (let i = 0; i < source.length; i++) {
|
|
91
|
+
const ch = source[i] ?? "";
|
|
92
|
+
const hex = source.slice(i + 1, i + 3);
|
|
93
|
+
if (ch === "=" && /^[0-9A-Fa-f]{2}$/.test(hex)) {
|
|
94
|
+
out.push(Number.parseInt(hex, 16));
|
|
95
|
+
i += 2;
|
|
96
|
+
} else if (underscoreSpace && ch === "_") out.push(32);
|
|
97
|
+
else for (const byte of new TextEncoder().encode(ch)) out.push(byte);
|
|
98
|
+
}
|
|
99
|
+
return new Uint8Array(out);
|
|
100
|
+
};
|
|
101
|
+
var decodeWords = (value) => value.replace(/\?=\s+=\?/g, "?==?").replace(
|
|
102
|
+
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
|
|
103
|
+
(_, _charset, kind, text) => utf8.decode(
|
|
104
|
+
kind.toUpperCase() === "B" ? base64Bytes(text) : quotedPrintableBytes(text, true)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
var splitHead = (raw) => {
|
|
108
|
+
const normalized = raw.replace(/\r\n/g, "\n");
|
|
109
|
+
const end = normalized.indexOf("\n\n");
|
|
110
|
+
const head = end < 0 ? normalized : normalized.slice(0, end);
|
|
111
|
+
const body = end < 0 ? "" : normalized.slice(end + 2);
|
|
112
|
+
const headers = {};
|
|
113
|
+
for (const line of head.replace(/\n[ \t]+/g, " ").split("\n")) {
|
|
114
|
+
const colon = line.indexOf(":");
|
|
115
|
+
if (colon <= 0) continue;
|
|
116
|
+
const name = line.slice(0, colon).trim().toLowerCase();
|
|
117
|
+
const value = decodeWords(line.slice(colon + 1).trim());
|
|
118
|
+
headers[name] = headers[name] ? `${headers[name]}, ${value}` : value;
|
|
119
|
+
}
|
|
120
|
+
return { headers, body };
|
|
121
|
+
};
|
|
122
|
+
var param = (header, name) => {
|
|
123
|
+
if (!header) return null;
|
|
124
|
+
const match = new RegExp(`${name}\\*?=(?:"([^"]*)"|([^;\\s]+))`, "i").exec(header);
|
|
125
|
+
return match ? match[1] ?? match[2] ?? null : null;
|
|
126
|
+
};
|
|
127
|
+
var decodeBody = (part) => {
|
|
128
|
+
const encoding = (part.headers["content-transfer-encoding"] ?? "").toLowerCase();
|
|
129
|
+
if (encoding === "base64") return base64Bytes(part.body);
|
|
130
|
+
if (encoding === "quoted-printable") return quotedPrintableBytes(part.body);
|
|
131
|
+
return new TextEncoder().encode(part.body);
|
|
132
|
+
};
|
|
133
|
+
var walk = (part, out) => {
|
|
134
|
+
const type = (part.headers["content-type"] ?? "text/plain").toLowerCase();
|
|
135
|
+
const boundary = param(part.headers["content-type"], "boundary");
|
|
136
|
+
if (type.startsWith("multipart/") && boundary) {
|
|
137
|
+
const sections = part.body.split(`--${boundary}`);
|
|
138
|
+
for (const section of sections.slice(1)) {
|
|
139
|
+
if (section.startsWith("--")) break;
|
|
140
|
+
walk(splitHead(section.replace(/^\n/, "")), out);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const disposition = part.headers["content-disposition"] ?? "";
|
|
145
|
+
const filename = param(disposition, "filename") ?? param(part.headers["content-type"], "name");
|
|
146
|
+
if (/^attachment/i.test(disposition) || filename && !type.startsWith("text/")) {
|
|
147
|
+
out.attachments.push({
|
|
148
|
+
filename: filename ?? "attachment",
|
|
149
|
+
contentType: type.split(";")[0]?.trim() ?? null,
|
|
150
|
+
size: decodeBody(part).byteLength
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const text = utf8.decode(decodeBody(part)).replace(/\n$/, "");
|
|
155
|
+
if (type.startsWith("text/html") && out.html === null) out.html = text;
|
|
156
|
+
else if (type.startsWith("text/plain") && out.text === null) out.text = text;
|
|
157
|
+
};
|
|
158
|
+
var addressList = (value) => (value ?? "").split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/).map((item) => item.trim()).filter(Boolean);
|
|
159
|
+
var parseMime = (raw) => {
|
|
160
|
+
const root = splitHead(raw);
|
|
161
|
+
const out = {
|
|
162
|
+
headers: root.headers,
|
|
163
|
+
subject: root.headers.subject ?? "",
|
|
164
|
+
from: root.headers.from ?? null,
|
|
165
|
+
to: addressList(root.headers.to),
|
|
166
|
+
cc: addressList(root.headers.cc),
|
|
167
|
+
html: null,
|
|
168
|
+
text: null,
|
|
169
|
+
attachments: []
|
|
170
|
+
};
|
|
171
|
+
walk(root, out);
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
174
|
+
var decodeRawMessage = (data) => {
|
|
175
|
+
const compact = data.replace(/\s+/g, "");
|
|
176
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(compact) && compact.length % 4 === 0) {
|
|
177
|
+
const decoded = utf8.decode(base64Bytes(compact));
|
|
178
|
+
if (/^[\x20-\x7e]+:/m.test(decoded)) return decoded;
|
|
179
|
+
}
|
|
180
|
+
return data;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/discovery.ts
|
|
184
|
+
var asServiceKind = (value) => typeof value === "string" && SERVICE_KINDS.includes(value) ? value : null;
|
|
185
|
+
var trimSlash = (url) => url.replace(/\/+$/, "");
|
|
186
|
+
var endpointsFromServices = (services, fetchImpl) => Object.entries(services).map(([name, input]) => {
|
|
187
|
+
if (typeof input === "string")
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
service: asServiceKind(name),
|
|
191
|
+
adminUrl: `${trimSlash(input)}/__admin`,
|
|
192
|
+
fetch: fetchImpl
|
|
193
|
+
};
|
|
194
|
+
if ("fetch" in input)
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
service: input.service ?? asServiceKind(name),
|
|
198
|
+
adminUrl: `http://${name}.mockingbird.local${input.adminPrefix ?? "/__admin"}`,
|
|
199
|
+
fetch: input.fetch
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
service: input.service ?? asServiceKind(name),
|
|
204
|
+
adminUrl: input.adminUrl ? trimSlash(input.adminUrl) : `${trimSlash(input.url)}/__admin`,
|
|
205
|
+
fetch: fetchImpl
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
var endpointsFromManifest = (manifest, fetchImpl, targets = {}) => Object.entries(manifest.services ?? {}).flatMap(([name, entry]) => {
|
|
209
|
+
if (!entry || entry.protocol && entry.protocol !== "http" || !entry.url) return [];
|
|
210
|
+
const service = asServiceKind(targets[name] ?? entry.target ?? name);
|
|
211
|
+
if (!service && (targets[name] ?? entry.target)) return [];
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
name,
|
|
215
|
+
service,
|
|
216
|
+
adminUrl: entry.adminUrl ? trimSlash(entry.adminUrl) : `${trimSlash(entry.url)}/__admin`,
|
|
217
|
+
fetch: fetchImpl
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
});
|
|
221
|
+
var discoverFleet = async (adminBase, fetchImpl, headers) => {
|
|
222
|
+
const base2 = trimSlash(adminBase);
|
|
223
|
+
const get = async (path) => {
|
|
224
|
+
const response = await fetchImpl(new Request(`${base2}${path}`, { headers }));
|
|
225
|
+
if (!response.ok) throw new Error(`fleet ${base2}${path}: HTTP ${response.status}`);
|
|
226
|
+
return await response.json();
|
|
227
|
+
};
|
|
228
|
+
const [root, namespaces] = await Promise.all([
|
|
229
|
+
get(""),
|
|
230
|
+
get("/namespaces").catch(() => ({}))
|
|
231
|
+
]);
|
|
232
|
+
const manifest = root.manifest;
|
|
233
|
+
if (!manifest?.services) throw new Error(`fleet ${base2}: discovery has no manifest`);
|
|
234
|
+
const described = root.services ?? {};
|
|
235
|
+
const targets = Object.fromEntries(
|
|
236
|
+
Object.entries(described).map(([name, entry]) => [name, entry?.target])
|
|
237
|
+
);
|
|
238
|
+
const names = Array.isArray(namespaces.namespaces) ? namespaces.namespaces.map((n) => n.namespace).filter((n) => typeof n === "string") : [];
|
|
239
|
+
return { endpoints: endpointsFromManifest(manifest, fetchImpl, targets), namespaces: names };
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/generated/browser.ts
|
|
243
|
+
var BROWSER_JS = 'var r={app:"notifications-app",topbar:"topbar",namespace:"namespace",search:"search",viewDevices:"view-devices",viewTimeline:"view-timeline",follow:"follow",sound:"sound-toggle",connection:"connection",source:"source",notice:"notice",toast:"toast",toastUndo:"toast-undo",smsDevice:"sms-device",smsRecipient:"sms-recipient",smsUnreadCount:"sms-unread-count",smsThread:"sms-thread",smsThreadName:"sms-thread-name",smsThreadPreview:"sms-thread-preview",smsThreadTime:"sms-thread-time",smsThreadUnread:"sms-thread-unread",smsThreadDelete:"sms-thread-delete",smsThreadMute:"sms-thread-mute",smsThreadPin:"sms-thread-pin",smsThreadToggleUnread:"sms-thread-toggle-unread",smsEmpty:"sms-empty",smsConversation:"sms-conversation",smsConversationTitle:"sms-conversation-title",smsBack:"sms-back",smsBubble:"sms-bubble",smsBubbleMedia:"sms-bubble-media",smsBubbleTime:"sms-bubble-time",smsTapback:"sms-tapback",smsTimestamp:"sms-timestamp",smsCode:"sms-code",smsMenu:"sms-menu",smsMenuCopy:"sms-menu-copy",smsMenuTapback:"sms-menu-tapback",pushDevice:"push-device",pushRecipient:"push-recipient",pushSkin:"push-skin",pushUnreadCount:"push-unread-count",pushNotification:"push-notification",pushNotificationApp:"push-notification-app",pushNotificationTitle:"push-notification-title",pushNotificationBody:"push-notification-body",pushNotificationTime:"push-notification-time",pushNotificationImage:"push-notification-image",pushNotificationData:"push-notification-data",pushNotificationMeta:"push-notification-meta",pushGroup:"push-group",pushGroupMore:"push-group-more",pushGroupCollapse:"push-group-collapse",pushClear:"push-clear",pushOptions:"push-options",pushPreview:"push-preview",pushSettings:"push-settings",pushClearAll:"push-clear-all",pushEmpty:"push-empty",emailDevice:"email-device",emailRecipient:"email-recipient",emailUnreadCount:"email-unread-count",emailRow:"email-row",emailRowFrom:"email-row-from",emailRowSubject:"email-row-subject",emailRowSnippet:"email-row-snippet",emailRowDate:"email-row-date",emailRowService:"email-row-service",emailRowAttachment:"email-row-attachment",emailRowStar:"email-row-star",emailRowArchive:"email-row-archive",emailRowDelete:"email-row-delete",emailRowToggleRead:"email-row-toggle-read",emailRefresh:"email-refresh",emailEmpty:"email-empty",emailOpen:"email-open",emailBack:"email-back",emailArchive:"email-archive",emailDelete:"email-delete",emailMarkUnread:"email-mark-unread",emailSubject:"email-subject",emailFromName:"email-from-name",emailFromAddress:"email-from-address",emailTo:"email-to",emailDate:"email-date",emailDetails:"email-details",emailDetail:"email-detail",emailBody:"email-body",emailText:"email-text",emailAttachments:"email-attachments",emailAttachment:"email-attachment",timeline:"timeline",timelineRow:"timeline-row",timelineEmpty:"timeline-empty",inspector:"inspector",inspectorTitle:"inspector-title",inspectorClose:"inspector-close",inspectorShowOnDevice:"inspector-show-on-device",inspectorField:"inspector-field",inspectorJson:"inspector-json"};var V=(d,p,h,k="")=>`<svg width="${d}" height="${p}" viewBox="0 0 ${d} ${p}" fill="none" aria-hidden="true" ${k}>${h}</svg>`,v=(d,p=20)=>`<svg width="${p}" height="${p}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="${d}"/></svg>`,c={signal:(d="currentColor")=>V(18,12,`<rect x="0" y="8" width="3" height="4" rx="1" fill="${d}"/><rect x="5" y="5.5" width="3" height="6.5" rx="1" fill="${d}"/><rect x="10" y="3" width="3" height="9" rx="1" fill="${d}"/><rect x="15" y="0" width="3" height="12" rx="1" fill="${d}"/>`),wifi:(d="currentColor")=>V(16,12,`<path d="M8 2.6c2.3 0 4.4.9 6 2.4l1.1-1.1A10 10 0 0 0 8 1 10 10 0 0 0 .9 3.9L2 5c1.6-1.5 3.7-2.4 6-2.4Z" fill="${d}"/><path d="M8 5.9c1.4 0 2.6.5 3.6 1.4l1.1-1.1A6.7 6.7 0 0 0 8 4.3c-1.8 0-3.5.7-4.7 1.9l1.1 1.1c1-.9 2.2-1.4 3.6-1.4Z" fill="${d}"/><path d="M10.3 8.6 8 11 5.7 8.6A3.3 3.3 0 0 1 8 7.6c.9 0 1.7.4 2.3 1Z" fill="${d}"/>`),battery:(d="currentColor")=>V(27,13,`<rect x=".5" y=".5" width="23" height="12" rx="3.8" stroke="${d}" stroke-opacity=".4"/><rect x="2" y="2" width="20" height="9" rx="2.5" fill="${d}"/><path d="M25 4.5v4c.8-.3 1.3-1.1 1.3-2s-.5-1.7-1.3-2Z" fill="${d}" fill-opacity=".45"/>`),compose:()=>V(24,24,\'<path d="M19 13v6.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 3 19.5v-13A1.5 1.5 0 0 1 4.5 5H11" stroke="#007aff" stroke-width="1.8" stroke-linecap="round"/><path d="m9.5 14.5.6-3.1 8.8-8.8a1.5 1.5 0 0 1 2.1 0l.9.9a1.5 1.5 0 0 1 0 2.1l-8.8 8.8-3.6.1Z" stroke="#007aff" stroke-width="1.8" stroke-linejoin="round"/>\'),search:(d="currentColor",p=16)=>V(p,p,`<circle cx="${p*.43}" cy="${p*.43}" r="${p*.3}" stroke="${d}" stroke-width="${p/9}"/><path d="m${p*.65} ${p*.65} ${p*.27} ${p*.27}" stroke="${d}" stroke-width="${p/9}" stroke-linecap="round"/>`),mic:(d="#8e8e93")=>V(14,20,`<rect x="4" y="1" width="6" height="11" rx="3" fill="${d}"/><path d="M1.5 9a5.5 5.5 0 0 0 11 0M7 14.5V18" stroke="${d}" stroke-width="1.6" stroke-linecap="round"/>`),chevronRight:(d="#c4c4c6")=>V(8,13,`<path d="m1.5 1.5 5 5-5 5" stroke="${d}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`),chevronBack:()=>V(13,21,\'<path d="M11 2 2.5 10.5 11 19" stroke="#007aff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>\'),plus:()=>V(16,16,\'<path d="M8 2v12M2 8h12" stroke="#8e8e93" stroke-width="2.2" stroke-linecap="round"/>\'),person:()=>V(26,26,\'<circle cx="13" cy="9" r="5" fill="#fff"/><path d="M3 24c0-5.5 4.5-9 10-9s10 3.5 10 9" fill="#fff"/>\'),padlock:()=>V(16,21,\'<rect x="1" y="9" width="14" height="11" rx="2.5" fill="#fff"/><path d="M4 9V6a4 4 0 0 1 8 0v3" stroke="#fff" stroke-width="2"/>\'),flashlight:()=>V(14,22,\'<path d="M2 1h10v4L9.5 9v11a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V9L2 5V1Z" fill="#fff"/>\'),camera:()=>V(22,18,\'<path d="M2 5.5A1.5 1.5 0 0 1 3.5 4h3l1.6-2.2h5.8L15.5 4h3A1.5 1.5 0 0 1 20 5.5v10a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 2 15.5v-10Z" fill="#fff"/><circle cx="11" cy="10.3" r="3.6" fill="#555"/>\'),bell:()=>v("M12 22a2 2 0 0 0 2-2h-4a2 2 0 0 0 2 2Zm6-6V11a6 6 0 0 0-5-5.9V4a1 1 0 0 0-2 0v1.1A6 6 0 0 0 6 11v5l-2 2v1h16v-1l-2-2Z",18),expand:()=>v("M7.4 8.6 12 13.2l4.6-4.6L18 10l-6 6-6-6z",16),collapse:()=>v("M7.4 15.4 12 10.8l4.6 4.6L18 14l-6-6-6 6z",16),androidWifi:()=>v("M12 21 23.6 6.5A17.6 17.6 0 0 0 12 2.5 17.6 17.6 0 0 0 .4 6.5Z",15),androidSignal:()=>v("M2 22h20V2Z",14),androidBattery:()=>v("M16 4h-2V2h-4v2H8a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1Z",15),menu:()=>v("M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z",24),tune:()=>v("M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z",24),help:()=>v("M11 18h2v-2h-2v2zm1-16a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-14a4 4 0 0 0-4 4h2a2 2 0 1 1 4 0c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5a4 4 0 0 0-4-4z",24),gear:()=>v("M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.48.48 0 0 0-.48-.41h-3.84a.48.48 0 0 0-.48.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.48.48 0 0 0-.59.22L2.74 8.87a.47.47 0 0 0 .12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.47.47 0 0 0-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1 1 12 8.4a3.6 3.6 0 0 1 0 7.2z",24),apps:()=>v("M6 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm6 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm-6 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0-6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm6 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm4-8a2 2 0 1 0 4 0 2 2 0 0 0-4 0zm-4 2a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm6 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",24),pencil:()=>v("M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",24),inbox:()=>v("M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12h-4a3 3 0 0 1-6 0H5V5h14v10z",20),star:()=>v("M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z",20),clock:()=>v("M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67V7z",20),send:()=>v("M3.4 20.4l17.45-7.48a1 1 0 0 0 0-1.84L3.4 3.6a.99.99 0 0 0-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z",20),draft:()=>v("M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM6 20V4h7v5h5v11H6z",20),more:()=>v("M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm0 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4z",20),refresh:()=>v("M17.65 6.35A7.96 7.96 0 0 0 12 4a8 8 0 1 0 7.73 10h-2.08A6 6 0 1 1 12 6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",20),checkbox:()=>v("M19 5v14H5V5h14m0-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",18),back:()=>v("M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z",20),archive:()=>v("M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5 6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z",20),report:()=>v("M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM19 14.9 14.9 19H9.1L5 14.9V9.1L9.1 5h5.8L19 9.1v5.8zM11 7h2v6h-2zm0 8h2v2h-2z",20),trash:()=>v("M15 4V3H9v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5zm2 15H7V6h10v13zM9 8h2v9H9zm4 0h2v9h-2z",20),reply:()=>v("M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z",20),forward:()=>v("M14 9V5l7 7-7 7v-4.1c-5 0-8.5 1.6-11 5.1 1-5 4-10 11-11z",20),clip:()=>v("M16.5 6v11.5a4 4 0 0 1-8 0V5a2.5 2.5 0 0 1 5 0v10.5a1 1 0 0 1-2 0V6H10v9.5a2.5 2.5 0 0 0 5 0V5a4 4 0 0 0-8 0v12.5a5.5 5.5 0 0 0 11 0V6h-1.5z",16),file:()=>v("M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm4 18H6V4h7v5h5v11z",28),dropdown:()=>v("M7 10l5 5 5-5z",18),lock:()=>v("M18 8h-1V6A5 5 0 0 0 7 6v2H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2zM9 6a3 3 0 0 1 6 0v2H9V6z",14),envelope:()=>V(30,24,\'<path d="M3 4h24v16a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4Z" fill="#e8eaed"/><path d="M3 4l12 9 12-9" stroke="#5f6368" stroke-width="2.2" stroke-linejoin="round"/><path d="M3 4h24" stroke="#5f6368" stroke-width="2.2"/>\'),pin:(d=16)=>v("M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z",d),bellSlash:(d=16)=>v("M20 18.69 7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11-.23.09-.46.2-.68.31L18 14.68z",d),volumeOn:(d=16)=>v("M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z",d),volumeOff:(d=16)=>v("M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z",d),markUnread:(d=20)=>v("M22 8.98V18c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h10.1c-.06.32-.1.66-.1 1 0 1.48.65 2.79 1.67 3.71L12 11 4 6v2l8 5 5.3-3.32c.54.2 1.1.32 1.7.32 1.13 0 2.16-.39 3-1.02zM16 5c0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3-3 1.34-3 3z",d),markRead:(d=20)=>v("M21.99 8c0-.72-.37-1.35-.94-1.7L12 1 2.95 6.3C2.38 6.65 2 7.28 2 8v10c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2l-.01-10zM12 13 3.74 7.84 12 3l8.26 4.84L12 13z",d),close:(d=16)=>v("M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",d),starFilled:(d=20)=>v("M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z",d)};var Q=(d,p,h,k,T,z=1)=>{for(let[$,N]of[[1,1],[2.76,.18],[5.4,.06]]){let A=d.createOscillator(),D=d.createGain();A.type="sine",A.frequency.value=k*$,D.gain.setValueAtTime(0,h),D.gain.linearRampToValueAtTime(.32*N*z,h+.006),D.gain.exponentialRampToValueAtTime(1e-4,h+T/$**.35),A.connect(D).connect(p),A.start(h),A.stop(h+T+.05)}},Ge=(d,p,h,k,T)=>{let z=d.createOscillator(),$=d.createBiquadFilter(),N=d.createGain();z.type="triangle",z.frequency.value=k,$.type="lowpass",$.frequency.setValueAtTime(k*6,h),$.frequency.exponentialRampToValueAtTime(k*1.5,h+T),N.gain.setValueAtTime(0,h),N.gain.linearRampToValueAtTime(.42,h+.004),N.gain.exponentialRampToValueAtTime(1e-4,h+T),z.connect($).connect(N).connect(p),z.start(h),z.stop(h+T+.05)},sa={sms:(d,p,h)=>{Q(d,p,h,1318.5,.5),Q(d,p,h+.12,1661.2,.5),Q(d,p,h+.24,1975.5,.8)},"push-ios":(d,p,h)=>{Q(d,p,h,1760,.35,.8),Q(d,p,h+.09,2349.3,.9,.9)},"push-android":(d,p,h)=>{Ge(d,p,h,1046.5,.22),Ge(d,p,h+.1,1568,.22),Ge(d,p,h+.2,2093,.45)},email:(d,p,h)=>{Q(d,p,h,987.8,.45,.7),Q(d,p,h+.16,1318.5,.9,.7)}},oa={sms:[180,90,180],"push-ios":[120],"push-android":[60,80,60]},zt=(d=!1)=>{let p=null,h=null,k=new Map,T=()=>{if(p)return p;let $=globalThis.AudioContext??globalThis.webkitAudioContext;if(!$)return null;try{p=new $,h=p.createGain(),h.gain.value=.6,h.connect(p.destination)}catch{p=null}return p},z={enabled:d,unlock(){if(!z.enabled)return;let $=T();$?.state==="suspended"&&$.resume().catch(()=>{})},play($){if(!z.enabled)return;let N=Date.now();if(N-(k.get($)??0)<600)return;k.set($,N);let A=oa[$];if(A)try{navigator.vibrate?.(A)}catch{}let D=T();!D||!h||(D.state==="suspended"&&D.resume().catch(()=>{}),sa[$](D,h,D.currentTime+.02))},close(){p?.close().catch(()=>{}),p=null}};return z};var Ke=(d,p)=>{let h=d.replace(/\\/\\*[\\s\\S]*?\\*\\//g,""),k="",T=0;for(;T<h.length;){let z=h.indexOf("{",T);if(z<0)break;let $=h.slice(T,z).trim(),N=1,A=z+1;for(;A<h.length&&N>0;)h[A]==="{"?N++:h[A]==="}"&&N--,A++;let D=h.slice(z+1,A-1);/^@(media|container|supports|layer)\\b/.test($)?k+=`${$} {\n${Ke(D,p)}}\n`:$.startsWith("@")?k+=`${$} {${D}}\n`:k+=`${na($,p)} {${D}}\n`,T=A}return k},na=(d,p)=>ra(d).map(h=>{let k=h.trim();return k===":root"||k==="html"||k==="body"?p:k.startsWith(":root")?`${p}${k.slice(5)}`:k.startsWith("&")?`${p}${k.slice(1)}`:`${p} ${k}`}).join(", "),ra=d=>{let p=[],h=0,k=0;for(let T=0;T<d.length;T++){let z=d[T];z==="("?h++:z===")"?h--:z===","&&h===0&&(p.push(d.slice(k,T)),k=T+1)}return p.push(d.slice(k)),p};var Te="mb-notifications",la=`\n:root {\n --bg: #0e1015; --panel: #161920; --panel-2: #1d212a; --line: #2a2f3a; --text: #e8eaf0;\n --muted: #9aa1b1; --accent: #0a84ff; --ok: #30d158; --bad: #ff453a; --warn: #ffd60a;\n --ios: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;\n --android: "Google Sans Text", "Google Sans", Roboto, "Segoe UI", system-ui, sans-serif;\n --mail: "Google Sans", Roboto, RobotoDraft, Helvetica, Arial, sans-serif;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n:root { display: block; position: relative; box-sizing: border-box; min-height: 100%; background: var(--bg); color: var(--text);\n font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-tap-highlight-color: transparent; }\n*, *::before, *::after { box-sizing: border-box; }\nbutton, input, select { font: inherit; color: inherit; }\nbutton { cursor: pointer; }\n[hidden] { display: none !important; }\n&[data-motion="reduce"] *, &[data-motion="reduce"] *::before, &[data-motion="reduce"] *::after { animation: none !important; transition: none !important; }\n&.embedded { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }\n&.embedded .mbn-scroll { flex: 1; min-height: 0; overflow: auto; }\n&.embedded .inspector { position: absolute; }\n[data-gesture]:focus-visible, [role="button"]:focus-visible, .icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n\n/* \\u2500\\u2500 Harness chrome \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.topbar { position: sticky; top: 0; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;\n padding: 10px 18px; background: rgba(14,16,21,.86); backdrop-filter: blur(14px); border-bottom: 1px solid var(--line); }\n.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; letter-spacing: -.01em; margin-right: 6px; }\n.brand-mark { width: 26px; height: 26px; border-radius: 7px; background: linear-gradient(135deg,#0a84ff,#5e5ce6); display: grid; place-items: center; }\n.brand small { color: var(--muted); font-weight: 500; }\n.field { display: flex; align-items: center; gap: 6px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 0 8px; height: 32px; }\n.field label { color: var(--muted); font-size: 12px; }\n.field select, .field input { background: transparent; border: 0; outline: 0; height: 30px; min-width: 0; }\n.field select option { background: var(--panel-2); }\n.field.search { flex: 1 1 220px; max-width: 360px; }\n.field.search input { width: 100%; }\n.tabs { display: flex; background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; padding: 2px; }\n.tabs button { background: none; border: 0; padding: 5px 12px; border-radius: 6px; color: var(--muted); font-weight: 550; }\n.tabs button[aria-pressed="true"] { background: #2c3240; color: var(--text); }\n.toggle { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; user-select: none; }\n.toggle input { accent-color: var(--accent); }\n.sources { display: flex; flex-wrap: wrap; gap: 6px; margin-left: auto; }\n.pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: 999px; border: 1px solid var(--line); background: var(--panel); font-size: 12px; color: var(--muted); white-space: nowrap; }\n.pill b { color: var(--text); font-weight: 600; }\n.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }\n.dot.ok { background: var(--ok); box-shadow: 0 0 0 3px rgba(48,209,88,.15); }\n.dot.bad { background: var(--bad); box-shadow: 0 0 0 3px rgba(255,69,58,.15); }\n.live { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }\n.notice { margin: 14px 18px 0; padding: 10px 14px; border: 1px solid #5a2b2b; background: #2a1517; color: #ffb4ab; border-radius: 10px; font-size: 13px; }\n\n.devices { display: flex; flex-wrap: wrap; gap: 28px; padding: 22px 18px 40px; align-items: flex-start; justify-content: center; }\n.devices.solo .device-col.wide { max-width: 1280px; }\n.device-col { display: flex; flex-direction: column; gap: 12px; align-items: center; width: 362px; flex: none; }\n.device-col.wide { flex: 1 1 720px; width: auto; min-width: 0; max-width: 1180px; align-items: stretch; }\n.device-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; width: 100%; justify-content: space-between; min-height: 32px; }\n.device-head .controls { display: flex; gap: 6px; align-items: center; min-width: 0; }\n.device-head h2 { margin: 0; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 650; display: flex; align-items: center; gap: 8px; }\n.device-head h2 .count { background: var(--accent); color: #fff; border-radius: 999px; padding: 0 7px; font-size: 11px; letter-spacing: 0; line-height: 18px; }\n.device-head .field { height: 30px; max-width: 250px; min-width: 0; }\n.device-head .field select { max-width: 190px; text-overflow: ellipsis; }\n.seg { display: flex; background: var(--panel-2); border: 1px solid var(--line); border-radius: 7px; padding: 2px; }\n.seg button { border: 0; background: none; color: var(--muted); padding: 2px 9px; border-radius: 5px; font-size: 12px; }\n.seg button[aria-pressed="true"] { background: #2c3240; color: var(--text); }\n\n/* \\u2500\\u2500 Phone hardware \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.iphone { position: relative; width: 362px; height: 760px; border-radius: 58px; padding: 12px; background: #0b0b0c;\n box-shadow: 0 0 0 2px #3a3a3c, 0 0 0 6px #1c1c1e, 0 0 0 7px #4a4a4e, 0 40px 80px rgba(0,0,0,.55); }\n.iphone::before, .iphone::after { content: ""; position: absolute; left: -9px; width: 4px; border-radius: 2px; background: #2c2c2e; }\n.iphone::before { top: 150px; height: 60px; }\n.iphone::after { top: 225px; height: 60px; }\n.iphone .screen { position: relative; width: 100%; height: 100%; border-radius: 47px; overflow: hidden; background: #fff; font-family: var(--ios); color: #000; -webkit-font-smoothing: antialiased; }\n.island { position: absolute; z-index: 30; top: 11px; left: 50%; transform: translateX(-50%); width: 112px; height: 33px; border-radius: 20px; background: #000; }\n.statusbar { position: absolute; z-index: 25; top: 0; left: 0; right: 0; height: 54px; display: flex; align-items: center; justify-content: space-between; padding: 6px 32px 0 40px; font: 600 16px/1 var(--ios); letter-spacing: -.01em; }\n.statusbar .icons { display: flex; gap: 6px; align-items: center; }\n.statusbar.light { color: #fff; }\n.home-indicator { position: absolute; z-index: 30; bottom: 8px; left: 50%; transform: translateX(-50%); width: 134px; height: 5px; border-radius: 3px; background: #000; }\n.home-indicator.light { background: rgba(255,255,255,.9); }\n\n.pixel { position: relative; width: 352px; height: 760px; border-radius: 44px; padding: 10px; background: #111;\n box-shadow: 0 0 0 2px #2b2b2f, 0 0 0 5px #3c3c41, 0 40px 80px rgba(0,0,0,.55); }\n.pixel::after { content: ""; position: absolute; right: -7px; top: 170px; width: 4px; height: 90px; border-radius: 2px; background: #2c2c2e; }\n.pixel .screen { position: relative; width: 100%; height: 100%; border-radius: 35px; overflow: hidden; font-family: var(--android); }\n.punch { position: absolute; z-index: 30; top: 12px; left: 50%; width: 13px; height: 13px; margin-left: -6px; border-radius: 50%; background: #050505; box-shadow: inset 0 0 0 2px #1a1a1a; }\n\n/* \\u2500\\u2500 Messages (iOS) \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.msgs { position: absolute; inset: 0; display: flex; flex-direction: column; background: #fff; }\n.msgs-nav { padding: 54px 16px 0; }\n.msgs-nav .row { display: flex; justify-content: space-between; align-items: center; height: 44px; color: #007aff; font-size: 17px; }\n.msgs-nav h1 { margin: 2px 0 8px; font: 700 34px/41px var(--ios); letter-spacing: .01em; }\n.ios-search { display: flex; align-items: center; gap: 6px; height: 36px; border-radius: 10px; background: rgba(118,118,128,.12); color: rgba(60,60,67,.6); padding: 0 8px; font-size: 17px; margin-bottom: 8px; }\n.ios-search span { flex: 1; }\n.threads { flex: 1; overflow-y: auto; }\n.thread { position: relative; display: flex; gap: 12px; padding: 10px 16px 0 26px; cursor: pointer; }\n.thread:active { background: #e5e5ea; }\n.thread .unread { position: absolute; left: 8px; top: 30px; width: 10px; height: 10px; border-radius: 50%; background: #007aff; }\n.avatar { flex: none; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; color: #fff; font: 600 18px var(--ios); background: linear-gradient(180deg,#a5abb8,#858994); }\n.thread .text { flex: 1; min-width: 0; border-bottom: .5px solid #c6c6c8; padding-bottom: 10px; }\n.thread .top { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }\n.thread .name { font: 600 17px/22px var(--ios); letter-spacing: -.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.thread .when { flex: none; color: rgba(60,60,67,.6); font-size: 15px; display: flex; align-items: center; gap: 6px; }\n.thread .preview { color: rgba(60,60,67,.6); font-size: 15px; line-height: 20px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; letter-spacing: -.01em; }\n.empty-ios { flex: 1; display: grid; place-items: center; color: rgba(60,60,67,.6); font-size: 17px; text-align: center; padding: 40px; }\n\n.convo { position: absolute; inset: 0; display: flex; flex-direction: column; background: #fff; }\n.convo-nav { position: relative; z-index: 5; padding-top: 50px; background: rgba(249,249,249,.94); backdrop-filter: blur(20px); border-bottom: .5px solid rgba(0,0,0,.18); }\n.convo-nav .bar { position: relative; height: 96px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }\n.convo-nav .back { position: absolute; left: 8px; top: 8px; display: flex; align-items: center; color: #007aff; font-size: 17px; background: none; border: 0; padding: 4px; }\n.convo-nav .back .badge { background: #007aff; color: #fff; border-radius: 10px; font-size: 13px; min-width: 20px; padding: 0 6px; line-height: 20px; margin-left: 2px; }\n.convo-nav .avatar { width: 54px; height: 54px; font-size: 22px; }\n.convo-nav .who { font-size: 12px; display: flex; align-items: center; gap: 2px; }\n.convo-body { flex: 1; overflow-y: auto; padding: 8px 0 12px; }\n.stamp { text-align: center; color: rgba(60,60,67,.6); font-size: 12px; margin: 14px 0 6px; }\n.stamp b { font-weight: 600; }\n.bubble-row { display: flex; padding: 0 14px; margin-top: 2px; }\n.bubble-row.gap { margin-top: 10px; }\n.bubble { position: relative; max-width: 74%; padding: 7px 12px 8px; border-radius: 18px; background: #e9e9eb; color: #000; font-size: 17px; line-height: 22px; letter-spacing: -.02em; white-space: pre-wrap; overflow-wrap: anywhere; }\n.bubble.tail::before { content: ""; position: absolute; bottom: 0; left: -7px; width: 20px; height: 20px; background: #e9e9eb; border-bottom-right-radius: 15px; }\n.bubble.tail::after { content: ""; position: absolute; bottom: 0; left: -10px; width: 10px; height: 20px; background: #fff; border-bottom-right-radius: 10px; }\n.bubble a, .bubble .code { color: inherit; text-decoration: underline; text-underline-offset: 2px; }\n.bubble .media { display: block; max-width: 100%; border-radius: 12px; margin: 2px 0 4px; }\n.bubble.arrive { animation: mbn-pop .35s cubic-bezier(.2,.9,.3,1.3); }\n.delivered-note { padding: 2px 16px 0 26px; color: rgba(60,60,67,.6); font-size: 11px; }\n.convo-input { display: flex; align-items: flex-end; gap: 8px; padding: 6px 10px 32px; background: #fff; }\n.convo-input .plus { width: 34px; height: 34px; border-radius: 50%; background: #e9e9eb; display: grid; place-items: center; flex: none; }\n.convo-input .field-ios { flex: 1; height: 36px; border: 1px solid #c6c6c8; border-radius: 18px; display: flex; align-items: center; justify-content: space-between; padding: 0 6px 0 14px; color: #c4c4c6; font-size: 17px; }\n.qt { margin: 0 10px 6px; padding: 8px 12px; border-radius: 10px; background: #d1d3d9; text-align: center; font-size: 15px; color: #000; }\n.qt small { display: block; font-size: 12px; color: #3c3c43; }\n\n/* \\u2500\\u2500 Lock screen (iOS) \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.lock { position: absolute; inset: 0; color: #fff; overflow: hidden;\n background: radial-gradient(120% 70% at 20% 10%, #6f86ff 0%, transparent 55%), radial-gradient(90% 60% at 90% 30%, #ff7eb3 0%, transparent 60%), radial-gradient(120% 80% at 50% 100%, #1d1b4a 0%, transparent 70%), linear-gradient(180deg,#3a3f9f,#141433); }\n.lock .padlock { position: absolute; top: 56px; left: 50%; transform: translateX(-50%); opacity: .95; }\n.lock .date { position: absolute; top: 84px; width: 100%; text-align: center; font: 600 19px var(--ios); letter-spacing: .01em; opacity: .92; }\n.lock .clock { position: absolute; top: 102px; width: 100%; text-align: center; font: 700 92px/1 "SF Pro Rounded", var(--ios); letter-spacing: -.02em; color: rgba(255,255,255,.93); }\n.lock-stack { position: absolute; left: 10px; right: 10px; top: 236px; bottom: 112px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; scrollbar-width: none; }\n.lock-stack::-webkit-scrollbar { display: none; }\n.lock-stack .center-label { text-align: center; font: 600 15px var(--ios); opacity: .85; padding: 4px 0; }\n.ios-note { position: relative; display: flex; gap: 10px; padding: 11px 12px 12px 11px; border-radius: 22px; color: #000; background: rgba(246,246,246,.66); backdrop-filter: blur(28px) saturate(180%); -webkit-backdrop-filter: blur(28px) saturate(180%); cursor: pointer; box-shadow: 0 1px 0 rgba(255,255,255,.25) inset; }\n.ios-note.arrive { animation: mbn-drop .45s cubic-bezier(.2,.9,.3,1.15); }\n.ios-note .app-icon { flex: none; width: 38px; height: 38px; border-radius: 9px; display: grid; place-items: center; color: #fff; font: 700 18px var(--ios); box-shadow: 0 0 0 .5px rgba(0,0,0,.08); }\n.ios-note .content { flex: 1; min-width: 0; }\n.ios-note .line1 { display: flex; justify-content: space-between; gap: 8px; }\n.ios-note .title { font: 600 15px/20px var(--ios); letter-spacing: -.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.ios-note .time { flex: none; font-size: 13px; color: rgba(60,60,67,.62); }\n.ios-note .body { font: 400 15px/20px var(--ios); letter-spacing: -.02em; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }\n.ios-note .thumb { flex: none; width: 38px; height: 38px; border-radius: 7px; object-fit: cover; align-self: center; }\n.ios-note.expanded .body { -webkit-line-clamp: unset; }\n.ios-note .hero { width: 100%; border-radius: 12px; margin-top: 8px; display: block; }\n.note-data { margin-top: 8px; font: 12px/1.45 var(--mono); background: rgba(255,255,255,.55); border-radius: 10px; padding: 8px 10px; white-space: pre-wrap; overflow-wrap: anywhere; color: #1c1c1e; }\n.note-meta { margin-top: 6px; font-size: 12px; color: rgba(60,60,67,.7); }\n.ios-stack-more { position: relative; height: 18px; margin: -14px 14px 0; border-radius: 0 0 18px 18px; background: rgba(246,246,246,.42); backdrop-filter: blur(20px); }\n.ios-stack-more + .ios-stack-more { margin: -10px 26px 0; background: rgba(246,246,246,.28); }\n.lock-buttons { position: absolute; bottom: 46px; left: 46px; right: 46px; display: flex; justify-content: space-between; }\n.lock-buttons span { width: 50px; height: 50px; border-radius: 50%; display: grid; place-items: center; background: rgba(0,0,0,.28); backdrop-filter: blur(20px); }\n.lock-empty { text-align: center; font: 500 15px var(--ios); opacity: .7; margin-top: 30px; }\n\n/* \\u2500\\u2500 Notification shade (Android) \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.shade { position: absolute; inset: 0; color: #e6e1e5; background: linear-gradient(180deg, rgba(20,18,24,.94), rgba(20,18,24,.98)), radial-gradient(100% 60% at 30% 0%, #4b3f72, #141218); overflow: hidden; }\n.shade .sb { position: absolute; top: 0; left: 0; right: 0; height: 40px; display: flex; align-items: center; justify-content: space-between; padding: 6px 24px 0; font: 500 14px var(--android); color: #e6e1e5; }\n.shade .sb .icons { display: flex; gap: 5px; align-items: center; }\n.shade .qs-head { position: absolute; top: 46px; left: 22px; right: 22px; display: flex; justify-content: space-between; align-items: center; font: 400 15px var(--android); color: #cac4d0; }\n.qs { position: absolute; top: 80px; left: 14px; right: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.qs .tile { height: 60px; border-radius: 20px; background: #36343b; display: flex; align-items: center; gap: 10px; padding: 0 14px; font: 500 14px var(--android); color: #e6e1e5; min-width: 0; }\n.qs .tile > div { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.qs .tile svg { flex: none; }\n.qs .tile.on { background: #d0bcff; color: #381e72; }\n.qs .tile small { display: block; font-weight: 400; font-size: 12px; opacity: .8; }\n.qs-bright { position: absolute; top: 216px; left: 14px; right: 14px; height: 44px; border-radius: 22px; background: #36343b; overflow: hidden; }\n.qs-bright::before { content: ""; position: absolute; inset: 0 44% 0 0; background: #d0bcff; border-radius: 22px; }\n.shade-list { position: absolute; top: 276px; left: 10px; right: 10px; bottom: 92px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; scrollbar-width: none; }\n.shade-list::-webkit-scrollbar { display: none; }\n.a-note { background: #2b2930; border-radius: 6px; padding: 14px 16px 14px 14px; cursor: pointer; display: flex; gap: 12px; }\n.a-note:first-child { border-top-left-radius: 26px; border-top-right-radius: 26px; }\n.a-note:last-child { border-bottom-left-radius: 26px; border-bottom-right-radius: 26px; }\n.a-note.arrive { animation: mbn-headsup .5s cubic-bezier(.2,.9,.3,1.12); }\n.a-note .a-icon { flex: none; width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; color: #fff; font: 600 15px var(--android); }\n.a-note .a-main { flex: 1; min-width: 0; }\n.a-note .a-head { display: flex; align-items: center; gap: 6px; font: 400 12px var(--android); color: #cac4d0; }\n.a-note .a-head .chev { margin-left: auto; width: 26px; height: 20px; border-radius: 10px; background: #49454f; display: grid; place-items: center; }\n.a-note .a-title { font: 500 15px/20px var(--android); color: #e6e1e5; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.a-note .a-body { font: 400 14px/20px var(--android); color: #cac4d0; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }\n.a-note.expanded .a-body { -webkit-line-clamp: unset; }\n.a-note .large-icon { flex: none; width: 40px; height: 40px; border-radius: 8px; object-fit: cover; align-self: center; }\n.a-note .big-picture { display: block; width: 100%; border-radius: 14px; margin-top: 10px; }\n.a-note .note-data { background: #1d1b20; color: #e6e1e5; }\n.a-note .note-meta { color: #938f99; }\n.shade-actions { position: absolute; bottom: 44px; left: 14px; right: 14px; display: flex; justify-content: space-between; }\n.shade-actions span { padding: 8px 16px; border-radius: 18px; background: #36343b; font: 500 14px var(--android); color: #e6e1e5; }\n.shade-empty { text-align: center; color: #938f99; font: 400 14px var(--android); margin-top: 26px; }\n.gesture { position: absolute; bottom: 12px; left: 50%; width: 110px; height: 4px; margin-left: -55px; border-radius: 2px; background: rgba(255,255,255,.75); }\n\n/* \\u2500\\u2500 Mail (web client) \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.window { position: relative; container-type: inline-size; border-radius: 12px; overflow: hidden; background: #f8fafd; box-shadow: 0 0 0 1px #2c313c, 0 30px 70px rgba(0,0,0,.5); height: 760px; display: flex; flex-direction: column; font-family: var(--mail); color: #1f1f1f; }\n.chrome { flex: none; height: 38px; background: #dfe3e9; display: flex; align-items: center; gap: 12px; padding: 0 12px; }\n.lights { display: flex; gap: 7px; }\n.lights i { width: 12px; height: 12px; border-radius: 50%; display: block; }\n.lights i:nth-child(1) { background: #ff5f57; } .lights i:nth-child(2) { background: #febc2e; } .lights i:nth-child(3) { background: #28c840; }\n.omnibox { flex: 1; max-width: 560px; margin: 0 auto; height: 26px; border-radius: 13px; background: #fff; display: flex; align-items: center; gap: 8px; padding: 0 12px; color: #5f6368; font: 13px system-ui; }\n.mail-top { flex: none; height: 64px; display: flex; align-items: center; gap: 8px; padding: 8px 16px 8px 12px; }\n.icon-btn { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: #444746; border: 0; background: none; flex: none; }\n.icon-btn:hover { background: rgba(68,71,70,.08); }\n.mail-logo { display: flex; align-items: center; gap: 6px; width: 194px; font: 400 22px var(--mail); color: #444746; letter-spacing: -.01em; }\n.mail-search { flex: 1; min-width: 0; max-width: 720px; height: 48px; border-radius: 24px; background: #e9eef6; display: flex; align-items: center; gap: 4px; padding: 0 4px; color: #444746; font-size: 16px; }\n.mail-search .placeholder { flex: 1; padding-left: 4px; }\n.mail-top .right { margin-left: auto; display: flex; align-items: center; gap: 2px; }\n.mail-avatar { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; color: #fff; font: 500 15px var(--mail); margin-left: 8px; }\n.mail-body { flex: 1; min-height: 0; display: flex; }\n.mail-nav { flex: none; width: 236px; padding: 8px 8px 0 0; overflow: hidden; }\n.compose { display: inline-flex; align-items: center; gap: 12px; height: 56px; padding: 0 22px 0 16px; margin: 0 0 16px 8px; border-radius: 16px; background: #c2e7ff; color: #001d35; font: 500 14px var(--mail); border: 0; box-shadow: none; }\n.nav-item { display: flex; align-items: center; gap: 18px; height: 32px; padding: 0 12px 0 26px; border-radius: 0 16px 16px 0; color: #202124; font: 400 14px var(--mail); }\n.nav-item.active { background: #d3e3fd; font-weight: 700; color: #001d35; }\n.nav-item .n { margin-left: auto; font-size: 12px; }\n.mail-main { flex: 1; min-width: 0; margin: 0 16px 16px 0; background: #fff; border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; }\n.mail-toolbar { flex: none; height: 48px; display: flex; align-items: center; gap: 4px; padding: 0 12px 0 14px; color: #444746; }\n.mail-toolbar .range { margin-left: auto; font-size: 12px; color: #444746; }\n.cat-tabs { flex: none; display: flex; border-bottom: 1px solid #f1f3f4; }\n.cat-tabs div { flex: 0 1 250px; min-width: 0; height: 54px; display: flex; align-items: center; gap: 16px; padding: 0 16px; color: #444746; font: 500 14px var(--mail); position: relative; }\n.cat-tabs div.on { color: #0b57d0; }\n.cat-tabs div.on::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: 0; height: 3px; border-radius: 3px 3px 0 0; background: #0b57d0; }\n.mail-list { flex: 1; overflow-y: auto; }\n.mrow { display: flex; align-items: center; height: 40px; padding: 0 16px 0 12px; border-bottom: 1px solid #f1f3f4; cursor: pointer; font-size: 14px; color: #202124; background: #f2f6fc; }\n.mrow.unread { background: #fff; font-weight: 700; }\n.mrow:hover { box-shadow: inset 1px 0 0 #dadce0, inset -1px 0 0 #dadce0, 0 1px 2px 0 rgba(60,64,67,.3), 0 1px 3px 1px rgba(60,64,67,.15); position: relative; z-index: 1; }\n.mrow.arrive { animation: mbn-rowflash 1.6s ease-out; }\n.mrow .chk, .mrow .star { flex: none; width: 20px; height: 20px; margin-right: 10px; color: #c4c7c5; display: grid; place-items: center; }\n.mrow .from { flex: none; width: 168px; padding-right: 24px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mrow .subj { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.mrow .snippet { color: #5f6368; font-weight: 400; }\n.mrow .clip { color: #5f6368; margin-left: 8px; flex: none; }\n.mrow .date { flex: none; width: 78px; text-align: right; font-size: 12px; }\n.mrow .svc { flex: none; font: 500 10px var(--mail); color: #5f6368; border: 1px solid #dadce0; border-radius: 4px; padding: 0 4px; margin-left: 8px; font-weight: 500; text-transform: uppercase; letter-spacing: .03em; }\n.mail-empty { padding: 60px 20px; text-align: center; color: #5f6368; font: 400 14px var(--mail); }\n.mail-empty b { display: block; font: 400 22px var(--mail); color: #1f1f1f; margin: 12px 0 4px; }\n.reader { flex: 1; overflow-y: auto; padding: 0 0 24px; }\n.reader .subject-row { display: flex; align-items: center; gap: 10px; padding: 18px 16px 8px 72px; }\n.reader h1 { margin: 0; font: 400 22px/28px var(--mail); color: #1f1f1f; }\n.label-chip { font: 400 12px var(--mail); background: #ddd; border-radius: 4px; padding: 0 6px; color: #444746; display: inline-flex; gap: 4px; align-items: center; }\n.sender-row { display: flex; gap: 14px; padding: 12px 16px 0 16px; }\n.sender-row .mail-avatar { width: 40px; height: 40px; font-size: 18px; margin: 0; }\n.sender-row .who { flex: 1; min-width: 0; }\n.sender-row .who b { font: 700 14px var(--mail); color: #1f1f1f; }\n.sender-row .who .addr { font: 400 12px var(--mail); color: #5e5e5e; }\n.sender-row .who .to { font: 400 12px var(--mail); color: #5e5e5e; display: flex; align-items: center; gap: 2px; margin-top: 2px; }\n.sender-row .meta { display: flex; align-items: center; gap: 4px; color: #5e5e5e; font: 400 12px var(--mail); white-space: nowrap; }\n.details { margin: 8px 16px 0 70px; padding: 10px 14px; border: 1px solid #dadce0; border-radius: 8px; font: 12px/1.6 var(--mail); color: #5e5e5e; display: grid; grid-template-columns: max-content 1fr; gap: 0 12px; }\n.details span:nth-child(odd) { text-align: right; }\n.details span:nth-child(even) { color: #1f1f1f; overflow-wrap: anywhere; }\n.mail-frame { display: block; width: calc(100% - 86px); margin: 16px 16px 0 70px; border: 0; min-height: 120px; background: #fff; }\n.mail-text { margin: 16px 16px 0 70px; white-space: pre-wrap; font: 400 14px/1.55 Arial, sans-serif; color: #222; overflow-wrap: anywhere; }\n.mail-text a { color: #1155cc; }\n.attachments { margin: 18px 16px 0 70px; border-top: 1px solid #e0e0e0; padding-top: 14px; }\n.attachments h3 { margin: 0 0 10px; font: 500 14px var(--mail); color: #1f1f1f; }\n.att-grid { display: flex; flex-wrap: wrap; gap: 12px; }\n.att { width: 180px; height: 104px; border: 1px solid #dadce0; border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; }\n.att .att-top { flex: 1; background: #f1f3f4; display: grid; place-items: center; color: #d93025; }\n.att .att-name { height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; font: 500 12px var(--mail); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.reply-row { display: flex; gap: 10px; margin: 24px 16px 0 70px; }\n.reply-row span { display: inline-flex; align-items: center; gap: 8px; padding: 0 22px 0 16px; height: 36px; border: 1px solid #747775; border-radius: 18px; font: 500 14px var(--mail); color: #444746; }\n\n/* \\u2500\\u2500 Timeline + inspector \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.timeline { padding: 18px; overflow-x: auto; }\n.tl td.mono:first-child { white-space: nowrap; }\n.tl { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }\n.tl th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 600; padding: 10px 12px; background: var(--panel-2); border-bottom: 1px solid var(--line); }\n.tl td { padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }\n.tl tr:last-child td { border-bottom: 0; }\n.tl tbody tr { cursor: pointer; }\n.tl tbody tr:hover { background: #1b1f28; }\n.tl .content { max-width: 520px; color: var(--muted); }\n.tl .content b { color: var(--text); font-weight: 600; }\n.tl .mono { font-family: var(--mono); font-size: 12px; }\n.chan { display: inline-flex; align-items: center; gap: 6px; padding: 2px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; }\n.chan.sms { background: rgba(48,209,88,.14); color: #6ee38a; }\n.chan.email { background: rgba(10,132,255,.16); color: #69b0ff; }\n.chan.push { background: rgba(255,159,10,.15); color: #ffc15e; }\n.svc-tag { font-size: 12px; color: var(--muted); }\n.empty-tl { padding: 60px; text-align: center; color: var(--muted); }\n.inspector { position: fixed; z-index: 40; top: 0; right: 0; bottom: 0; width: min(560px, 100vw); background: var(--panel); border-left: 1px solid var(--line); box-shadow: -20px 0 50px rgba(0,0,0,.4); display: flex; flex-direction: column; }\n.inspector header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--line); }\n.inspector header h3 { margin: 0; font-size: 15px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.inspector .btn { background: var(--panel-2); border: 1px solid var(--line); border-radius: 7px; padding: 4px 10px; font-size: 13px; }\n.inspector pre { margin: 0; flex: 1; overflow: auto; padding: 14px 16px; font: 12px/1.55 var(--mono); color: #cdd3df; white-space: pre-wrap; overflow-wrap: anywhere; }\n.inspector .kv { display: grid; grid-template-columns: 110px 1fr; gap: 6px 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); font-size: 13px; }\n.inspector .kv span:nth-child(odd) { color: var(--muted); }\n.inspector .kv span:nth-child(even) { overflow-wrap: anywhere; font-family: var(--mono); font-size: 12px; }\n\n\n/* \\u2500\\u2500 Sound toggle \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.icon-toggle { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--muted); font-size: 13px; }\n.icon-toggle[aria-pressed="true"] { color: var(--text); border-color: #3d4a66; }\n\n/* \\u2500\\u2500 Swipe gestures \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500 */\n.swipe { position: relative; touch-action: pan-y; }\n.swipe > .swipe-card { position: relative; z-index: 1; will-change: transform; }\n.swipe.settling > .swipe-card, .settling.a-note { transition: transform .32s cubic-bezier(.2,.9,.25,1), opacity .32s ease; }\n.swipe-actions { position: absolute; inset: 0; display: flex; justify-content: flex-end; align-items: stretch; visibility: hidden; }\n.swipe.swiping > .swipe-actions, .swipe.open > .swipe-actions { visibility: visible; }\n.collapsing { overflow: hidden; transition: height .26s ease, margin .26s ease, padding .26s ease, opacity .2s ease !important; }\n\n/* iOS lock screen: Options / Clear behind a card */\n.ios-swipe .swipe-actions { gap: 8px; }\n.ios-swipe .swipe-actions button { width: 72px; border: 0; border-radius: 20px; background: rgba(246,246,246,.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); color: #000; font: 500 14px var(--ios); }\n.push-group { display: flex; flex-direction: column; gap: 8px; }\n.group-head { display: flex; align-items: center; gap: 8px; padding: 0 2px; animation: mbn-fadein .25s ease; }\n.group-head h3 { flex: 1; margin: 0; font: 700 20px/1.2 var(--ios); color: #fff; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.group-head button { border: 0; border-radius: 15px; background: rgba(246,246,246,.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); color: #000; font: 500 13px var(--ios); padding: 6px 12px; }\n.group-head button.x { width: 30px; height: 30px; padding: 0; display: grid; place-items: center; border-radius: 50%; }\n.push-group[data-open="true"] .ios-swipe { animation: mbn-unstack .38s cubic-bezier(.2,.9,.3,1.1) backwards; }\n.push-group[data-open="true"] .ios-swipe:nth-child(3) { animation-delay: .03s; } .push-group[data-open="true"] .ios-swipe:nth-child(4) { animation-delay: .06s; }\n.stack-layers { cursor: pointer; border: 0; padding: 0; background: none; display: block; width: 100%; }\n.stack-layers .ios-stack-more:first-child { margin-top: -14px; }\n.lock.wake { animation: mbn-wake 1.1s ease-out; }\n.ios-overlay { position: absolute; inset: 0; z-index: 40; display: flex; flex-direction: column; justify-content: center; gap: 10px; padding: 0 14px; background: rgba(255,255,255,.28); backdrop-filter: blur(18px) saturate(140%); -webkit-backdrop-filter: blur(18px) saturate(140%); animation: mbn-fadein .2s ease; }\n.ios-overlay.dark { background: rgba(0,0,0,.28); }\n.ios-overlay .ios-note { animation: mbn-lift .3s cubic-bezier(.2,.9,.3,1.2); box-shadow: 0 20px 40px rgba(0,0,0,.25); }\n.ios-menu { align-self: flex-start; width: 250px; border-radius: 14px; overflow: hidden; background: rgba(237,237,237,.86); backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(30px); box-shadow: 0 10px 40px rgba(0,0,0,.2); animation: mbn-lift .28s cubic-bezier(.2,.9,.3,1.2); }\n.ios-menu button { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 11px 16px; border: 0; border-bottom: .5px solid rgba(60,60,67,.29); background: none; color: #000; font: 400 17px var(--ios); letter-spacing: -.02em; text-align: left; }\n.ios-menu button:last-child { border-bottom: 0; }\n.ios-menu button:active { background: rgba(0,0,0,.08); }\n.ios-overlay.dark .ios-menu { align-self: stretch; width: auto; }\n\n/* Android shade */\n.a-note { touch-action: pan-y; position: relative; }\n.a-settings { margin-top: 8px; display: grid; gap: 8px; animation: mbn-fadein .2s ease; }\n.a-settings .opt { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 16px; border: 1px solid #49454f; font: 500 14px var(--android); color: #e6e1e5; background: none; text-align: left; }\n.a-settings .opt[aria-pressed="true"] { background: #4a4458; border-color: #d0bcff; }\n.a-settings .opt small { display: block; font-weight: 400; color: #cac4d0; font-size: 12px; }\n.a-settings .row { display: flex; justify-content: space-between; }\n.a-settings .row button { border: 0; background: none; color: #d0bcff; font: 500 14px var(--android); padding: 6px 4px; }\n.shade-actions button { padding: 8px 16px; border: 0; border-radius: 18px; background: #36343b; font: 500 14px var(--android); color: #e6e1e5; }\n\n/* Messages: thread swipe actions, page transitions, bubble times, tapbacks */\n.thread-swipe .swipe-actions { justify-content: space-between; }\n.thread-swipe .swipe-actions .side { display: flex; }\n.thread-swipe .swipe-actions button { width: 74px; border: 0; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; font: 400 13px var(--ios); }\n.act-unread { background: #007aff; } .act-pin { background: #ffcc00; } .act-mute { background: #5856d6; } .act-delete { background: #ff3b30; }\n.thread-swipe .thread { background: #fff; }\n.thread .name-line { display: flex; align-items: center; gap: 4px; min-width: 0; }\n.thread .flag { flex: none; color: rgba(60,60,67,.6); display: inline-flex; }\n.msgs.enter { animation: mbn-back .38s cubic-bezier(.2,.9,.25,1); }\n.convo.enter { animation: mbn-push .4s cubic-bezier(.2,.9,.25,1); box-shadow: -10px 0 30px rgba(0,0,0,.12); }\n.convo-body { --reveal: 0px; overflow-x: hidden; touch-action: pan-y; }\n.convo-body .bubble-row, .convo-body .stamp { transform: translateX(calc(-1 * var(--reveal))); }\n.convo-body.settling .bubble-row, .convo-body.settling .stamp { transition: transform .3s cubic-bezier(.2,.9,.25,1); }\n.bubble-row { position: relative; }\n.bubble-time { position: absolute; right: -66px; top: 50%; transform: translateY(-50%); width: 58px; color: rgba(60,60,67,.6); font-size: 12px; white-space: nowrap; }\n.bubble { touch-action: pan-y; -webkit-user-select: text; user-select: text; }\n.tapback { position: absolute; z-index: 2; top: -16px; right: -12px; min-width: 30px; height: 30px; padding: 0 4px; border-radius: 15px; background: #007aff; color: #fff; display: grid; place-items: center; font: 700 13px/1 var(--ios); box-shadow: 0 0 0 2.5px #fff; animation: mbn-pop .35s cubic-bezier(.2,.9,.3,1.4); }\n.tapback-bar { align-self: flex-start; display: flex; gap: 4px; padding: 6px 8px; border-radius: 26px; background: rgba(233,233,235,.96); box-shadow: 0 8px 30px rgba(0,0,0,.18); animation: mbn-lift .28s cubic-bezier(.2,.9,.3,1.3); }\n.tapback-bar button { width: 38px; height: 38px; border-radius: 50%; border: 0; background: none; color: #8e8e93; font: 700 16px/1 var(--ios); display: grid; place-items: center; }\n.tapback-bar button[aria-pressed="true"] { background: #007aff; color: #fff; }\n.ios-overlay .bubble { align-self: flex-start; max-width: 80%; animation: mbn-lift .28s cubic-bezier(.2,.9,.3,1.2); }\n\n/* Mail: hover actions, row swipe, toast */\n.mrow .hover-actions { display: none; flex: none; align-items: center; gap: 2px; margin-left: 8px; }\n.mrow .hover-actions .icon-btn { width: 32px; height: 32px; }\n.mrow:hover .hover-actions, .mrow:focus-within .hover-actions { display: flex; }\n.mrow:hover .date, .mrow:focus-within .date { display: none; }\n.mrow .star { cursor: pointer; }\n.mrow .star[aria-pressed="true"] { color: #f4b400; }\n.mrow-swipe { background: #188038; }\n.mrow-swipe .swipe-actions { justify-content: space-between; align-items: center; padding: 0 22px; color: #fff; }\n.toast { position: absolute; z-index: 20; left: 24px; bottom: 24px; display: flex; align-items: center; gap: 24px; min-width: 288px; max-width: calc(100% - 48px); padding: 14px 16px 14px 24px; border-radius: 4px; background: #303030; color: #f2f2f2; font: 400 14px var(--mail); box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px rgba(0,0,0,.14); animation: mbn-toast .25s cubic-bezier(0,0,.2,1); }\n.toast button { margin-left: auto; border: 0; background: none; color: #a8c7fa; font: 500 14px var(--mail); padding: 4px 8px; }\n.reader.enter, .mail-list.enter { animation: mbn-fadein .18s ease; }\n\n@keyframes mbn-fadein { from { opacity: 0; } to { opacity: 1; } }\n@keyframes mbn-headsup { from { opacity: 0; transform: translateY(-70px) scale(.98); } to { opacity: 1; transform: none; } }\n@keyframes mbn-unstack { from { opacity: 0; transform: translateY(-40px) scale(.94); } to { opacity: 1; transform: none; } }\n@keyframes mbn-wake { from { filter: brightness(.25); } to { filter: none; } }\n@keyframes mbn-lift { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: none; } }\n@keyframes mbn-push { from { transform: translateX(100%); } to { transform: none; } }\n@keyframes mbn-back { from { transform: translateX(-28%); filter: brightness(.92); } to { transform: none; filter: none; } }\n@keyframes mbn-toast { from { opacity: 0; transform: translateY(16px) scale(.96); } to { opacity: 1; transform: none; } }\n@keyframes mbn-pop { from { transform: scale(.6) translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }\n@keyframes mbn-drop { from { transform: translateY(-30px) scale(.96); opacity: 0; } to { transform: none; opacity: 1; } }\n@keyframes mbn-fade { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }\n@keyframes mbn-rowflash { from { background: #fef7e0; } to { background: #fff; } }\n@container (max-width: 980px) { .mail-nav { width: 72px; } .mail-nav .lbl, .mail-nav .n, .compose .lbl { display: none; } .mail-logo { width: auto; } .compose { padding: 0 16px; } .mrow .svc { display: none; } }\n@container (max-width: 640px) { .mail-nav, .mail-search, .cat-tabs, .mail-logo .lbl, .mail-toolbar .range { display: none; } .mail-main { margin-left: 8px; } .mrow .from { width: 110px; } .reader .subject-row, .sender-row { padding-left: 16px; } .mail-frame, .mail-text, .attachments, .reply-row, .details { margin-left: 16px; } .mail-frame { width: calc(100% - 32px); } .sender-row { flex-wrap: wrap; } .sender-row .who { flex-basis: calc(100% - 54px); } .sender-row .meta { flex-basis: 100%; padding-left: 54px; white-space: normal; } .reader h1 { font-size: 19px; line-height: 24px; } }\n@media (max-width: 760px) { .devices { padding: 16px 8px; } .device-col.wide { flex-basis: 100%; } }\n`,ha=Ke(la,`.${Te}`);var Lt=[{id:"heart",glyph:"\\u2665",label:"Heart"},{id:"like",glyph:"\\u{1F44D}",label:"Like"},{id:"dislike",glyph:"\\u{1F44E}",label:"Dislike"},{id:"laugh",glyph:"HA<br>HA",label:"Laugh"},{id:"emphasize",glyph:"\\u203C",label:"Emphasize"},{id:"question",glyph:"?",label:"Question"}],fe=["sms","push","email"];function At(d,p={}){let h=p.live??!0,k=p.syncUrl??(h&&!p.fetch&&!p.baseUrl),T=p.storage??h,z=p.now,$=()=>z??Date.now(),N=p.baseUrl??(p.fetch||typeof location>"u"?"http://harness.local/":location.href),A=(e,t)=>{let s=new Request(new URL(e,N),t);return p.fetch?p.fetch(s):fetch(s)},D=new AbortController,P=(e,t,s=!1)=>d.addEventListener(e,t,{signal:D.signal,capture:s}),he=new Set,G=(e,t)=>{let s=setTimeout(()=>{he.delete(s),t()},e);he.add(s)},ne=!1,Y=new URLSearchParams(p.search??(k&&typeof location<"u"?location.search:"")),ee=Y.get("view"),j=p.initial??{},a={namespace:Y.get("namespace")??p.namespace??"default",namespaces:[],items:[],sources:[],discoveryError:null,version:0,view:ee==="timeline"?"timeline":"devices",only:ee==="sms"||ee==="email"||ee==="push"?ee:null,search:"",follow:Y.get("follow")!=="0",connected:!1,sms:{to:null,thread:null,menu:null,transition:null},push:{to:null,skin:"auto",expanded:null,preview:null,settings:null,wake:!1},email:{to:null,open:null,transition:!1},inspect:null,toast:null,arrived:new Set(p.arrived??[])};Object.assign(a,{...j,sms:{...a.sms,...j.sms},push:{...a.push,...j.push},email:{...a.email,...j.email},toast:j.toast?{text:j.toast}:null}),ee==="timeline"&&(a.view="timeline"),delete a.local,delete a.push.openGroups;let w={dismissed:new Set(j.local?.dismissed??[]),pinned:new Set(j.local?.pinned??[]),muted:new Set(j.local?.muted??[]),starred:new Set(j.local?.starred??[]),silent:new Set(j.local?.silent??[]),checked:new Set,tapbacks:new Map(Object.entries(j.local?.tapbacks??{})),openGroups:new Set(j.push?.openGroups??[])},re=j.mailDetailsOpen??!1,Ye=Y.get("to"),Ht=Y.get("from"),_e=Y.get("id"),O=Ye&&a.only?{channel:a.only,to:Ye}:null;Y.get("follow")===null&&O&&(a.follow=!1);let Ct=()=>typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches,We=()=>(p.motion??"auto")==="auto"?Ct()?"reduce":"full":p.motion,W=()=>We()==="full",Ze="mockingbird-notifications:sound",Dt=()=>{if(!T)return null;try{let e=localStorage.getItem(Ze);return e===null?null:e==="1"}catch{return null}},K=zt(Y.get("sound")!==null?Y.get("sound")==="1":Dt()??p.sound??!1),Xe=()=>`mockingbird-notifications:seen:${a.namespace}`,H=new Set(p.seen??[]),Je=()=>{if(T)try{H=new Set(JSON.parse(localStorage.getItem(Xe())??"[]"))}catch{H=new Set}},Qe=()=>{if(T)try{localStorage.setItem(Xe(),JSON.stringify([...H].slice(-5e3)))}catch{}},le=e=>{let t=!1;for(let s of e)H.has(s)||(H.add(s),t=!0);t&&Qe()},ze=e=>{for(let t of e)H.delete(t);Qe()},o=e=>String(e??"").replace(/[&<>"\']/g,t=>`&#${t.charCodeAt(0)};`),l=e=>`data-testid="${e}"`,te=e=>`data-notification-id="${o(e.id)}" data-channel="${e.channel}" data-service="${o(e.service)}" data-source="${o(e.source)}" data-to="${o(e.to)}" data-from="${o(e.from.address)}" data-status="${o(e.status??"")}" data-created-at="${o(e.createdAt)}" data-unread="${!H.has(e.id)}"`,et=e=>e.replace(/\\bhttps?:\\/\\/[^\\s<]+[^\\s<.,;:!?)\'"]/g,t=>`<a href="${t}" target="_blank" rel="noreferrer">${t}</a>`),Le=e=>{let t=/^\\+1(\\d{3})(\\d{3})(\\d{4})$/.exec(e);return t?`+1 (${t[1]}) ${t[2]}-${t[3]}`:e},tt=e=>/^\\+?\\d[\\d\\s()-]{5,}$/.test(e),be=e=>e.from.name??(tt(e.from.address)?Le(e.from.address):e.from.address),ge=e=>{let t=e.replace(/[^\\p{L}\\p{N}\\s]/gu," ").trim().split(/\\s+/).filter(Boolean);return t.length?((t[0]?.[0]??"")+(t[1]?.[0]??"")).toUpperCase():"?"},Nt=["#1a73e8","#e8710a","#188038","#d93025","#9334e6","#12b5cb","#e52592","#f9ab00","#5f6368","#a142f4"],at=["#ff9500","#34c759","#5856d6","#ff2d55","#007aff","#af52de","#ff3b30","#30b0c7"],Et=e=>[...e].reduce((t,s)=>t*31+s.charCodeAt(0)>>>0,7),xe=(e,t=Nt)=>t[Et(e)%t.length]??"#5f6368",de=(e,t)=>e.toDateString()===t.toDateString(),Z=e=>e.toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}),Rt=e=>{let t=new Date(e),s=new Date($());if(de(t,s))return Z(t);let n=new Date(s);return n.setDate(s.getDate()-1),de(t,n)?"Yesterday":s.getTime()-t.getTime()<6*864e5?t.toLocaleDateString([],{weekday:"long"}):t.toLocaleDateString([],{year:"2-digit",month:"numeric",day:"numeric"})},jt=e=>{let t=new Date(e),s=new Date($());return de(t,s)?`<b>Today</b> ${Z(t)}`:`<b>${o(t.toLocaleDateString([],{weekday:"short",month:"short",day:"numeric"}))}</b> at ${Z(t)}`},Ae=e=>{let t=Math.max(0,($()-Date.parse(e))/1e3);return t<60?"now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`},Vt=e=>{let t=Math.max(0,($()-Date.parse(e))/1e3);return t<60?"now":t<3600?`${Math.floor(t/60)}m`:t<86400?`${Math.floor(t/3600)}h`:`${Math.floor(t/86400)}d`},It=e=>{let t=new Date(e);return de(t,new Date($()))?t.toLocaleTimeString():t.toLocaleString([],{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",...t.getFullYear()===new Date($()).getFullYear()?{}:{year:"numeric"}})},Pt=e=>{let t=new Date(e),s=new Date($());return de(t,s)?Z(t):t.getFullYear()===s.getFullYear()?t.toLocaleDateString([],{month:"short",day:"numeric"}):t.toLocaleDateString([],{year:"2-digit",month:"numeric",day:"numeric"})},Ot=e=>`${new Date(e).toLocaleString([],{weekday:"short",month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})} (${Ae(e)==="now"?"0 minutes ago":Ae(e)})`,ae={twilio:"Twilio",resend:"Resend",fcm:"FCM",ses:"SES",sns:"SNS",customerio:"Customer.io",mailosaur:"Mailosaur"},Ut=e=>{let t=a.search.trim().toLowerCase();return t?[e.to,e.from.address,e.from.name??"",e.preview,e.email?.subject??"",e.push?.title??"",e.source,e.service].join(`\n`).toLowerCase().includes(t):!0},it=()=>a.items.filter(Ut),U=e=>it().filter(t=>t.channel===e&&!w.dismissed.has(t.id)),He=e=>{let t=new Map;for(let s of U(e)){let n=t.get(s.to)??{to:s.to,count:0,unread:0,latest:s.createdAt};n.count++,H.has(s.id)||n.unread++,s.createdAt>n.latest&&(n.latest=s.createdAt),s.push&&(n.platform=s.push.platform),t.set(s.to,n)}return[...t.values()].sort((s,n)=>s.latest<n.latest?1:-1)},Ce=()=>{for(let e of fe){let t=He(e),s=a[e];if(O?.channel===e){s.to=O.to;continue}(!s.to||!t.some(n=>n.to===s.to))&&(s.to=t[0]?.to??null)}},De=async e=>{let t=await A(e);if(!t.ok)throw new Error(`${e}: HTTP ${t.status}`);return await t.json()},ve=async()=>{let e=new URLSearchParams({namespace:a.namespace,order:"newest"}),t=await De(`./api/notifications?${e}`);ne||(a.items=t.notifications,a.version=t.version??a.version,Ce(),F())},st=async()=>{try{let e=await De("./api/sources");a.sources=e.sources,a.discoveryError=e.discoveryError;let t=await De("./api/namespaces");a.namespaces=t.namespaces,ne||X()}catch{}},ie=(e,t)=>`${e}|${t}`,Ne=(e,t)=>`${e}|${t}`,Bt=e=>{if(e.channel==="sms")return w.muted.has(ie(e.to,e.from.address))?null:"sms";if(e.channel==="email")return"email";let t=a.push.skin==="auto"?e.push?.platform==="android"?"android":"ios":a.push.skin;return t==="android"&&w.silent.has(e.push?.app??"App")?null:t==="android"?"push-android":"push-ios"},qt=e=>{for(let n of e)a.arrived.add(n.id);let t=[...e].sort((n,i)=>n.createdAt<i.createdAt?1:-1);for(let n of fe){let i=t.filter(b=>b.channel===n),u=O?.channel===n?i.find(b=>b.to===O.to):i[0];u&&(O?.channel===n?n==="sms"&&!a.sms.thread&&(a.sms.thread=u.from.address):a.follow&&(a[n].to=u.to,n==="sms"&&a.sms.thread!==null&&(a.sms.thread=u.from.address)),n==="push"&&a.push.to===u.to&&(a.push.wake=!0))}let s=new Set(e.map(Bt).filter(n=>n!==null));for(let n of s)K.play(n);p.onArrive?.(e),G(1800,()=>{for(let n of e)a.arrived.delete(n.id)})},Ee=null,ot=()=>{Ee?.abort();let e=new AbortController;Ee=e;let t=a.namespace,s=(i,u)=>{if(!e.signal.aborted){if(i==="sources")a.sources=JSON.parse(u),X();else if(i==="change"){let{added:b,version:f}=JSON.parse(u);a.version=f,qt(b),ve().catch(()=>{})}}};(async()=>{for(;!e.signal.aborted;){try{let i=await A(`./api/events?namespace=${encodeURIComponent(t)}`,{signal:e.signal,headers:{accept:"text/event-stream"}});if(!i.ok||!i.body)throw new Error(`events: HTTP ${i.status}`);a.connected=!0,X();let u=i.body.pipeThrough(new TextDecoderStream).getReader(),b="";for(;;){let{value:f,done:y}=await u.read();if(y)break;b+=f.replace(/\\r\\n/g,`\n`);let g=b.indexOf(`\n\n`);for(;g>=0;){let m=b.slice(0,g);b=b.slice(g+2);let x="message",L=[];for(let S of m.split(`\n`))S.startsWith("event:")?x=S.slice(6).trim():S.startsWith("data:")&&L.push(S.slice(5).trimStart());L.length&&s(x,L.join(`\n`)),g=b.indexOf(`\n\n`)}}}catch{}if(e.signal.aborted)return;a.connected=!1,X(),await new Promise(i=>G(1e3,()=>i(null)))}})()};d.classList.add(Te),p.embedded&&d.classList.add("embedded"),d.dataset.testid=r.app,d.dataset.ready="false",d.innerHTML=`\n<header class="topbar" data-region="topbar" ${l(r.topbar)}></header>\n<div data-region="notice"></div>\n<div class="mbn-scroll" data-region="scroll">\n<section class="devices" data-region="devices">\n <div class="device-col" data-region="col-sms"></div>\n <div class="device-col" data-region="col-push"></div>\n <div class="device-col wide" data-region="col-email"></div>\n</section>\n<section class="timeline" data-region="timeline" hidden></section>\n</div>\n<aside class="inspector" data-region="inspector" ${l(r.inspector)} hidden></aside>`;let B=e=>d.querySelector(`[data-region="${e}"]`),nt=e=>d.querySelector(`[data-testid="${e}"]`),rt=()=>p.title??d.dataset.title??"Notifications",Ft=()=>{Object.assign(d.dataset,{view:a.view,namespace:a.namespace,count:String(a.items.length),version:String(a.version),connected:String(a.connected),sound:String(K.enabled),motion:We()})},X=()=>{Ft();let e=B("topbar");if(e.hidden=p.chrome===!1,p.chrome===!1)return;let t=[...new Set([...a.namespaces,a.namespace])].sort(),s=document.activeElement===nt(r.search);if(e.innerHTML=`\n <div class="brand"><span class="brand-mark">${c.bell()}</span><span>${o(rt())} <small>Mockingbird</small></span></div>\n <label class="field">Namespace <select data-action="namespace" ${l(r.namespace)} aria-label="Namespace">${t.map(i=>`<option ${i===a.namespace?"selected":""}>${o(i)}</option>`).join("")}</select></label>\n <div class="field search">${c.search("#9aa1b1",14)}<input type="search" data-action="search" ${l(r.search)} aria-label="Filter notifications" placeholder="Filter by recipient, sender, text\\u2026" value="${o(a.search)}"></div>\n <div class="tabs" role="group" aria-label="View">\n <button data-action="view" data-value="devices" ${l(r.viewDevices)} aria-pressed="${a.view==="devices"}">Devices</button>\n <button data-action="view" data-value="timeline" ${l(r.viewTimeline)} aria-pressed="${a.view==="timeline"}">Timeline <span style="opacity:.6">${a.items.length}</span></button>\n </div>\n <label class="toggle" title="Jump each device to the newest arrival"><input type="checkbox" data-action="follow" ${l(r.follow)} ${a.follow?"checked":""}> Follow new</label>\n <button class="icon-toggle" data-action="sound" ${l(r.sound)} aria-pressed="${K.enabled}" title="Play alert sounds on arrivals">${K.enabled?c.volumeOn():c.volumeOff()}<span>Sound</span></button>\n <span class="live" ${l(r.connection)} data-state="${a.connected?"live":"reconnecting"}" role="status"><span class="dot ${a.connected?"ok":"bad"}"></span>${a.connected?"Live":"Reconnecting"}</span>\n <div class="sources">${a.sources.map(i=>`<span class="pill" title="${o(i.error??i.url)}" ${l(r.source)} data-name="${o(i.name)}" data-service="${o(i.service??"")}" data-ok="${i.ok}" data-count="${i.count}"><span class="dot ${i.ok?"ok":i.error?"bad":""}"></span>${o(i.service&&i.name!==i.service?`${i.name} \\xB7 ${ae[i.service]??i.service}`:ae[i.name]??i.name)} <b>${i.count}</b></span>`).join("")}</div>`,s){let i=nt(r.search);i?.focus(),i?.setSelectionRange(i.value.length,i.value.length)}let n=a.sources.filter(i=>i.error);B("notice").innerHTML=a.discoveryError||n.length?`<div class="notice" ${l(r.notice)} data-kind="error" role="alert">${a.discoveryError?`Fleet discovery failed: ${o(a.discoveryError)}. `:""}${n.map(i=>`<b>${o(i.name)}</b>: ${o(i.error)}`).join(" \\xB7 ")}</div>`:a.sources.length?"":`<div class="notice" ${l(r.notice)} data-kind="empty" style="border-color:#3a3f4b;background:#171a21;color:#9aa1b1">No messaging emulators found yet. Start the fleet with twilio, resend, fcm, ses, sns, customerio or mailosaur.</div>`},Re=(e,t,s,n)=>{let i=He(e),u=a[e].to;return`<label class="field">${t}<select data-action="recipient" data-channel="${e}" ${l(s)} aria-label="${t}" ${i.length?"":"disabled"}>${i.length?i.map(b=>`<option value="${o(b.to)}" ${b.to===u?"selected":""}>${b.unread?"\\u25CF ":""}${o(n(b.to))} \\xB7 ${b.count}</option>`).join(""):"<option>No recipients yet</option>"}</select></label>`},je=(e,t)=>e?`<span class="count" ${l(t)} data-count="${e}">${e}</span>`:"",lt=(e=!1)=>`\n <div class="statusbar ${e?"light":""}"><span>${Z(new Date($())).replace(/\\s?[AP]M$/i,"")}</span>\n <span class="icons">${c.signal()}${c.wifi()}${c.battery()}</span></div>`,Ve=(e,t)=>{if(!W()||typeof e.animate!="function")return t();let s=new Map;for(let n of e.querySelectorAll("[data-flip]"))s.set(n.dataset.flip??"",n.getBoundingClientRect().top);t();for(let n of e.querySelectorAll("[data-flip]")){let i=s.get(n.dataset.flip??"");if(i===void 0)continue;let u=i-n.getBoundingClientRect().top;Math.abs(u)<1||n.animate([{transform:`translateY(${u}px)`},{transform:"none"}],{duration:380,easing:"cubic-bezier(.2,.9,.25,1)"})}},Ie=e=>{let t=new Map;for(let s of U("sms").filter(n=>n.to===e)){let n=s.from.address;t.set(n,[...t.get(n)??[],s])}return[...t.entries()].map(([s,n])=>({from:s,items:n.sort((i,u)=>i.createdAt<u.createdAt?-1:1),pinned:w.pinned.has(ie(e,s))})).sort((s,n)=>s.pinned!==n.pinned?s.pinned?-1:1:(s.items.at(-1)?.createdAt??"")<(n.items.at(-1)?.createdAt??"")?1:-1)},dt=e=>{let t=e?be(e):"";return tt(e?.from.address??"")&&!e?.from.name?c.person():o(ge(t))},Pe=e=>e.sms?.code??(/\\b(code|otp|passcode|pin|verif)/i.test(e.sms?.body??"")?/\\b(\\d{4,8})\\b/.exec(e.sms?.body??"")?.[1]??null:null),Gt=(e,t)=>t?e.replace(new RegExp(`\\\\b${t}\\\\b`),`<span class="code">${t}</span>`):e,Kt=e=>{let t=w.tapbacks.get(e),s=Lt.find(n=>n.id===t);return s?`<span class="tapback" ${l(r.smsTapback)} data-tapback="${s.id}" aria-label="${s.label}">${s.glyph}</span>`:""},ct=e=>{let t=Gt(et(o(e.sms?.body??"")),Pe(e));return(e.sms?.media??[]).map(n=>`<img class="media" src="${o(n)}" alt="" ${l(r.smsBubbleMedia)}>`).join("")+t},q=()=>{let e=a.sms.to,t=e?Ie(e):[],s=U("sms").filter(g=>!H.has(g.id)).length,n,i=t.find(g=>g.from===a.sms.thread),u=a.sms.transition;if(a.sms.transition=null,i&&e){le(i.items.map(R=>R.id));let g=i.items[0],m=t.filter(R=>R!==i).reduce((R,se)=>R+se.items.filter(oe=>!H.has(oe.id)).length,0),x=0,L=!0,S=i.items.map((R,se)=>{let oe=Date.parse(R.createdAt),kt=se===0||oe-x>15*6e4?`<div class="stamp" ${l(r.smsTimestamp)}>${se===0?"Text Message<br>":""}${jt(R.createdAt)}</div>`:"",Mt=i.items[se+1],St=!Mt||Date.parse(Mt.createdAt)-oe>6e4,ia=!kt&&L&&se>0;x=oe,L=St;let Tt=Pe(R);return`${kt}<div class="bubble-row ${ia?"gap":""}"><div class="bubble ${St?"tail":""} ${a.arrived.has(R.id)?"arrive":""}" ${te(R)} ${l(r.smsBubble)} ${Tt?`data-code="${o(Tt)}"`:""} data-longpress="sms-bubble" tabindex="0" aria-haspopup="menu">${ct(R)}${Kt(R.id)}</div><span class="bubble-time" ${l(r.smsBubbleTime)} aria-hidden="true">${o(Z(new Date(oe)))}</span></div>`}).join(""),C=i.items.at(-1),J=C?Pe(C):null,Se=a.sms.menu?i.items.find(R=>R.id===a.sms.menu):null,aa=w.muted.has(ie(e,i.from));n=`<div class="convo ${u==="push"&&W()?"enter":""}" ${l(r.smsConversation)} data-from="${o(i.from)}">\n <div class="convo-nav"><div class="bar">\n <button class="back" data-action="sms-back" ${l(r.smsBack)} aria-label="Back to Messages">${c.chevronBack()}${m?`<span class="badge">${m}</span>`:""}</button>\n <div class="avatar">${dt(g)}</div>\n <div class="who" ${l(r.smsConversationTitle)}>${o(g?be(g):i.from)}${aa?` <span class="flag">${c.bellSlash(12)}</span>`:""} ${c.chevronRight("#8e8e93")}</div>\n </div></div>\n <div class="convo-body" data-swipe="sms-convo">${S}</div>\n ${J&&C&&$()-Date.parse(C.createdAt)<10*6e4?`<button class="qt" data-action="sms-copy-code" data-code="${o(J)}" ${l(r.smsCode)}><small>From Messages</small>${o(J)}</button>`:""}\n <div class="convo-input"><span class="plus">${c.plus()}</span><span class="field-ios">Text Message<span>${c.mic()}</span></span></div>\n ${Se?Yt(Se):""}\n </div>`}else a.sms.thread=null,a.sms.menu=null,n=`<div class="msgs ${u==="back"&&W()?"enter":""}">\n <div class="msgs-nav"><div class="row"><span>Edit</span>${c.compose()}</div><h1>Messages</h1>\n <div class="ios-search">${c.search("rgba(60,60,67,.6)",15)}<span>Search</span>${c.mic("rgba(60,60,67,.6)")}</div></div>\n ${t.length&&e?`<div class="threads" role="list">${t.map(g=>{let m=g.items.at(-1),x=g.items.some(J=>!H.has(J.id)),L=ie(e,g.from),S=w.muted.has(L),C=m?be(m):g.from;return`<div class="swipe thread-swipe" data-swipe="sms-thread" data-from="${o(g.from)}" data-flip="thread:${o(g.from)}" role="listitem">\n <div class="swipe-actions">\n <div class="side"><button class="act-unread" data-action="thread-unread" data-from="${o(g.from)}" ${l(r.smsThreadToggleUnread)}>${x?c.markRead(22):c.markUnread(22)}${x?"Read":"Unread"}</button><button class="act-pin" data-action="thread-pin" data-from="${o(g.from)}" ${l(r.smsThreadPin)}>${c.pin(22)}${g.pinned?"Unpin":"Pin"}</button></div>\n <div class="side"><button class="act-mute" data-action="thread-mute" data-from="${o(g.from)}" ${l(r.smsThreadMute)}>${c.bellSlash(22)}${S?"Show Alerts":"Hide Alerts"}</button><button class="act-delete" data-action="thread-delete" data-from="${o(g.from)}" ${l(r.smsThreadDelete)}>${c.trash()}Delete</button></div>\n </div>\n <div class="thread swipe-card" data-action="sms-open" data-from="${o(g.from)}" ${l(r.smsThread)} ${m?te(m):""} data-unread="${x}" data-pinned="${g.pinned}" data-muted="${S}" role="button" tabindex="0" aria-label="${o(C)}${x?", unread":""}">\n ${x?`<span class="unread" ${l(r.smsThreadUnread)}></span>`:""}<div class="avatar">${dt(m)}</div>\n <div class="text"><div class="top"><span class="name-line"><span class="name" ${l(r.smsThreadName)}>${o(C)}</span>${g.pinned?`<span class="flag">${c.pin(13)}</span>`:""}${S?`<span class="flag">${c.bellSlash(13)}</span>`:""}</span><span class="when" ${l(r.smsThreadTime)}>${o(Rt(m?.createdAt??""))} ${c.chevronRight()}</span></div>\n <div class="preview" ${l(r.smsThreadPreview)}>${o(m?.sms?.body??"")}</div></div></div></div>`}).join("")}</div>`:`<div class="empty-ios" ${l(r.smsEmpty)}>${e?"No Messages":"No text messages yet.<br><br>SMS sent through Twilio, SNS, Customer.io or Mailosaur shows up here."}</div>`}\n </div>`;let b=B("col-sms");Ve(b,()=>{b.innerHTML=`\n <div class="device-head"><h2>Text messages ${je(s,r.smsUnreadCount)}</h2>${Re("sms","Phone",r.smsRecipient,Le)}</div>\n <div class="iphone" ${l(r.smsDevice)} data-screen="${i?"conversation":"threads"}" data-recipient="${o(e??"")}" data-thread="${o(i?.from??"")}"><div class="screen"><div class="island"></div>${lt()}${n}<div class="home-indicator"></div></div></div>`});let f=b.querySelector(".convo-body"),y=()=>{f&&(f.scrollTop=f.scrollHeight)};y(),requestAnimationFrame(y)},Yt=e=>{let t=w.tapbacks.get(e.id);return`<div class="ios-overlay" data-action="close-overlay" ${l(r.smsMenu)} data-notification-id="${o(e.id)}" role="dialog" aria-label="Message actions">\n <div class="tapback-bar" role="toolbar" aria-label="Tapback">${Lt.map(s=>`<button data-action="tapback" data-id="${o(e.id)}" data-value="${s.id}" ${l(r.smsMenuTapback)} data-tapback="${s.id}" aria-label="${s.label}" aria-pressed="${t===s.id}">${s.glyph}</button>`).join("")}</div>\n <div class="bubble tail">${ct(e)}</div>\n <div class="ios-menu" role="menu">\n <button role="menuitem" data-action="close-overlay">Reply</button>\n <button role="menuitem" data-action="sms-copy" data-id="${o(e.id)}" ${l(r.smsMenuCopy)}>Copy</button>\n <button role="menuitem" data-action="close-overlay">Translate</button>\n <button role="menuitem" data-action="close-overlay">More\\u2026</button>\n </div></div>`},pt=e=>{let t=e.push?.data??{};return Object.keys(t).length?`<div class="note-data" ${l(r.pushNotificationData)}>${o(JSON.stringify(t,null,2))}</div>`:""},mt=e=>`<div class="note-meta" ${l(r.pushNotificationMeta)}>${o(ae[e.service]??e.service)} \\xB7 ${o(e.status??"accepted")}${e.push?.badge!==null&&e.push?.badge!==void 0?` \\xB7 badge ${e.push.badge}`:""}${e.push?.sound?` \\xB7 sound ${o(e.push.sound)}`:""}</div>`,ut=(e,t={})=>`${te(e)} ${l(r.pushNotification)} data-platform="${o(e.push?.platform??"unknown")}" data-app="${o(e.push?.app??"App")}" ${Object.entries(t).map(([s,n])=>`data-${s}="${o(n)}"`).join(" ")}`,ft=(e,t)=>{let s=e.push,n=s?.app??"App",i=t.expanded;return`<div class="ios-note swipe-card ${i?"expanded":""} ${a.arrived.has(e.id)?"arrive":""}" data-action="push-toggle" data-id="${o(e.id)}" ${ut(e,{expanded:i,stacked:t.stacked??!1})} ${t.extra??""} role="button" tabindex="0" aria-expanded="${i}" data-longpress="ios-note">\n <span class="app-icon" style="background:${xe(n,at)}" ${l(r.pushNotificationApp)} aria-label="${o(n)}">${o(ge(n).slice(0,1))}</span>\n <div class="content">\n <div class="line1"><span class="title" ${l(r.pushNotificationTitle)}>${o(s?.title??n)}</span><span class="time" ${l(r.pushNotificationTime)}>${o(Ae(e.createdAt))}</span></div>\n <div class="body" ${l(r.pushNotificationBody)}>${o(s?.body??"")}</div>\n ${i&&s?.imageUrl?`<img class="hero" src="${o(s.imageUrl)}" alt="" ${l(r.pushNotificationImage)}>`:""}\n ${i?pt(e)+mt(e):""}\n </div>\n ${!i&&s?.imageUrl?`<img class="thumb" src="${o(s.imageUrl)}" alt="" ${l(r.pushNotificationImage)}>`:""}\n </div>`},we=(e,t={})=>`<div class="swipe ios-swipe" data-swipe="ios-note" data-id="${o(e.id)}" data-flip="note:${o(e.id)}" ${t.hidden?"hidden":""}>\n <div class="swipe-actions"><button data-action="push-options" data-id="${o(e.id)}" ${l(r.pushOptions)}>Options</button><button data-action="push-clear" data-id="${o(e.id)}" ${l(r.pushClear)}>Clear</button></div>\n ${ft(e,{expanded:a.push.expanded===e.id,stacked:t.stacked??!1,extra:t.top?\'data-group-top="true"\':""})}\n </div>`,_t=e=>{let t=e.push,s=a.push.expanded===e.id,n=t?.app??"App",i=a.push.settings===e.id,u=w.silent.has(n);return`<div class="a-note ${s?"expanded":""} ${a.arrived.has(e.id)?"arrive":""}" data-action="push-toggle" data-id="${o(e.id)}" ${ut(e,{expanded:s})} data-swipe="a-note" data-longpress="a-note" data-flip="note:${o(e.id)}" role="button" tabindex="0" aria-expanded="${s}">\n <span class="a-icon" style="background:${xe(n,at)}" aria-hidden="true">${o(ge(n).slice(0,1))}</span>\n <div class="a-main">\n <div class="a-head"><span ${l(r.pushNotificationApp)}>${o(n)}</span><span>\\u2022</span><span ${l(r.pushNotificationTime)}>${o(Vt(e.createdAt))}</span>${u?`<span>${c.bellSlash(12)}</span>`:""}<span class="chev">${s?c.collapse():c.expand()}</span></div>\n ${t?.title?`<div class="a-title" ${l(r.pushNotificationTitle)}>${o(t.title)}</div>`:""}\n <div class="a-body" ${l(r.pushNotificationBody)}>${o(t?.body??"")}</div>\n ${s&&t?.imageUrl?`<img class="big-picture" src="${o(t.imageUrl)}" alt="" ${l(r.pushNotificationImage)}>`:""}\n ${s?pt(e)+mt(e):""}\n ${i?`<div class="a-settings" ${l(r.pushSettings)} data-app="${o(n)}">\n <button class="opt" data-action="push-alerting" data-app="${o(n)}" data-value="alerting" aria-pressed="${!u}">${c.bell()}<span>Default<small>May ring or vibrate based on device settings</small></span></button>\n <button class="opt" data-action="push-alerting" data-app="${o(n)}" data-value="silent" aria-pressed="${u}">${c.bellSlash(18)}<span>Silent<small>No sound or vibration</small></span></button>\n <div class="row"><button data-action="close-overlay">Turn off notifications</button><button data-action="close-overlay">Done</button></div></div>`:""}\n </div>\n ${!s&&t?.imageUrl?`<img class="large-icon" src="${o(t.imageUrl)}" alt="" ${l(r.pushNotificationImage)}>`:""}\n </div>`},Wt=(e,t)=>{let s=[],n=new Map;for(let i of t){let u=i.push?.app??"App";n.has(u)||s.push(u),n.set(u,[...n.get(u)??[],i])}return s.map(i=>{let u=n.get(i)??[],b=u[0];if(!b)return"";if(u.length===1)return we(b);let f=Ne(e,i),y=w.openGroups.has(f),g=u.length-1;return y?`<div class="push-group" ${l(r.pushGroup)} data-app="${o(i)}" data-count="${u.length}" data-open="true" data-flip="group:${o(i)}">\n <div class="group-head"><h3>${o(i)}</h3><button data-action="group-collapse" data-key="${o(f)}" ${l(r.pushGroupCollapse)}>Show less</button><button class="x" data-action="group-clear" data-key="${o(f)}" data-app="${o(i)}" aria-label="Clear ${o(i)} notifications">${c.close(14)}</button></div>\n ${u.map(m=>we(m)).join("")}</div>`:`<div class="push-group" ${l(r.pushGroup)} data-app="${o(i)}" data-count="${u.length}" data-open="false" data-flip="group:${o(i)}">\n ${we(b,{top:!0})}\n <button class="stack-layers" data-action="group-open" data-key="${o(f)}" ${l(r.pushGroupMore)} data-count="${g}" aria-label="Show ${g} more notification${g===1?"":"s"}"><div class="ios-stack-more"></div>${g>1?\'<div class="ios-stack-more"></div>\':""}<div class="center-label" style="font-size:13px;margin-top:6px">${g} more notification${g===1?"":"s"}</div></button>\n ${u.slice(1).map(m=>we(m,{stacked:!0,hidden:!0})).join("")}</div>`}).join("")},E=()=>{let e=a.push.to,t=e?U("push").filter(x=>x.to===e):[];le(t.map(x=>x.id));let s=t[0]?.push?.platform??"unknown",n=a.push.skin==="auto"?s==="android"?"android":"ios":a.push.skin,i=new Date($()),u=U("push").filter(x=>!H.has(x.id)).length,b=a.push.wake&&W();a.push.wake=!1;let f,y=`${l(r.pushDevice)} data-skin="${n}" data-recipient="${o(e??"")}" data-platform="${o(s)}"`;if(n==="android")f=`<div class="pixel" ${y}><div class="screen"><div class="punch"></div><div class="shade">\n <div class="sb"><span>${Z(i).replace(/\\s?[AP]M$/i,"")}</span><span class="icons">${c.androidWifi()}${c.androidSignal()}${c.androidBattery()}</span></div>\n <div class="qs-head"><span>${o(i.toLocaleDateString([],{weekday:"short",month:"short",day:"numeric"}))}</span>${c.gear()}</div>\n <div class="qs"><div class="tile on">${c.androidWifi()}<div>Internet<small>Mockingbird</small></div></div><div class="tile">${c.bell()}<div>Do Not Disturb<small>Off</small></div></div>\n <div class="tile">${c.androidBattery()}<div>Battery Saver<small>Off</small></div></div><div class="tile">${c.lock()}<div>Screen lock<small>On</small></div></div></div>\n <div class="qs-bright"></div>\n <div class="shade-list" role="list">${t.length?t.map(_t).join(""):`<div class="shade-empty" ${l(r.pushEmpty)}>No notifications</div>`}</div>\n ${t.length?`<div class="shade-actions"><button data-action="close-overlay">Manage</button><button data-action="push-clear-all" ${l(r.pushClearAll)}>Clear all</button></div>`:""}\n <div class="gesture"></div></div></div></div>`;else{let x=a.push.preview?t.find(L=>L.id===a.push.preview):null;f=`<div class="iphone" ${y}><div class="screen"><div class="island"></div>${lt(!0)}<div class="lock ${b?"wake":""}">\n <div class="padlock">${c.padlock()}</div>\n <div class="date">${o(i.toLocaleDateString([],{weekday:"long",month:"long",day:"numeric"}))}</div>\n <div class="clock">${o(Z(i).replace(/\\s?[AP]M$/i,""))}</div>\n <div class="lock-stack" role="list">${t.length&&e?`<div class="center-label">Notification Center</div>${Wt(e,t)}`:`<div class="lock-empty" ${l(r.pushEmpty)}>${e?"No Older Notifications":"No push notifications yet"}</div>`}</div>\n <div class="lock-buttons"><span>${c.flashlight()}</span><span>${c.camera()}</span></div>\n ${x?`<div class="ios-overlay dark" data-action="close-overlay" ${l(r.pushPreview)} data-notification-id="${o(x.id)}" role="dialog" aria-label="Notification preview">${ft(x,{expanded:!0})}\n <div class="ios-menu" role="menu"><button role="menuitem" data-action="push-clear" data-id="${o(x.id)}">Clear</button><button role="menuitem" data-action="close-overlay">Options</button></div></div>`:""}\n </div><div class="home-indicator light"></div></div></div>`}let g=x=>{let L=He("push").find(C=>C.to===x);return`${x.length>22?`${x.slice(0,10)}\\u2026${x.slice(-8)}`:x}${L?.platform&&L.platform!=="unknown"?` \\xB7 ${L.platform}`:""}`},m=B("col-push");Ve(m,()=>{m.innerHTML=`\n <div class="device-head"><h2>Push ${je(u,r.pushUnreadCount)}</h2>\n <div class="controls">${Re("push","Device",r.pushRecipient,g)}\n <div class="seg" role="group" aria-label="Phone">${["auto","ios","android"].map(x=>`<button data-action="skin" data-value="${x}" ${l(r.pushSkin)} aria-pressed="${a.push.skin===x}">${x==="auto"?"Auto":x==="ios"?"iOS":"Android"}</button>`).join("")}</div></div></div>${f}`})},Zt=e=>`<!doctype html><html><head><meta charset="utf-8"><base target="_blank"><style>body{margin:0;font:14px/1.5 Arial,Helvetica,sans-serif;color:#222;overflow-wrap:anywhere}img{max-width:100%;height:auto}</style></head><body>${e.email?.html??""}</body></html>`,Xt=(e,t)=>{a.toast=t?{text:e,undo:t}:{text:e},I();let s=a.toast;G(6e3,()=>{a.toast===s&&(a.toast=null,I())})},ht=(e,t)=>{w.dismissed.add(e),a.email.open===e&&(a.email.open=null,a.email.transition=!0),Xt(t==="archived"?"Conversation archived.":"Conversation moved to Trash.",()=>{w.dismissed.delete(e),a.toast=null,I()})},I=()=>{let e=a.email.to,t=e?U("email").filter(m=>m.to===e):[],s=t.filter(m=>!H.has(m.id)).length,n=U("email").filter(m=>!H.has(m.id)).length,i=t.find(m=>m.id===a.email.open);i||(a.email.open=null);let u=a.email.transition&&W();a.email.transition=!1;let b=e??"you@example.test",f;if(i){le([i.id]);let m=i.email,x=i.from.name??i.from.address,L=i.to.toLowerCase()===b.toLowerCase()?"me":i.to,S=(C,J,Se)=>`<span>${J}:</span><span ${l(r.emailDetail)} data-field="${C}">${o(Se)}</span>`;f=`<div class="mail-toolbar">\n <button class="icon-btn" data-action="mail-back" ${l(r.emailBack)} title="Back to Inbox" aria-label="Back to Inbox">${c.back()}</button>\n <button class="icon-btn" data-action="mail-archive" data-id="${o(i.id)}" ${l(r.emailArchive)} title="Archive" aria-label="Archive">${c.archive()}</button><span class="icon-btn">${c.report()}</span><button class="icon-btn" data-action="mail-delete" data-id="${o(i.id)}" ${l(r.emailDelete)} title="Delete" aria-label="Delete">${c.trash()}</button>\n <button class="icon-btn" data-action="mail-unread" data-id="${o(i.id)}" ${l(r.emailMarkUnread)} title="Mark as unread" aria-label="Mark as unread">${c.markUnread()}</button><span class="icon-btn">${c.clock()}</span><span class="icon-btn">${c.more()}</span>\n <span class="range">${t.indexOf(i)+1} of ${t.length}</span></div>\n <div class="reader ${u?"enter":""}" ${te(i)} ${l(r.emailOpen)}>\n <div class="subject-row"><h1 ${l(r.emailSubject)}>${o(m?.subject||"(no subject)")}</h1><span class="label-chip">Inbox</span></div>\n <div class="sender-row">\n <span class="mail-avatar" style="background:${xe(i.from.address)}">${o(ge(x).slice(0,1))}</span>\n <div class="who"><b ${l(r.emailFromName)}>${o(x)}</b> <span class="addr"><<span ${l(r.emailFromAddress)}>${o(i.from.address)}</span>></span>\n <div class="to" data-action="mail-details" ${l(r.emailTo)} role="button" tabindex="0" aria-expanded="${re}">to ${o(L)}${m?.cc.length?`, ${o(m.cc.join(", "))}`:""} ${c.dropdown()}</div></div>\n <div class="meta"><span ${l(r.emailDate)}>${o(Ot(i.createdAt))}</span> <span class="icon-btn">${c.star()}</span><span class="icon-btn">${c.reply()}</span><span class="icon-btn">${c.more()}</span></div>\n </div>\n ${re?`<div class="details" ${l(r.emailDetails)}>\n ${S("from","from",i.from.name?`${i.from.name} <${i.from.address}>`:i.from.address)}\n ${S("to","to",i.to)}\n ${m?.cc.length?S("cc","cc",m.cc.join(", ")):""}\n ${m?.bcc.length?S("bcc","bcc",m.bcc.join(", ")):""}\n ${m?.replyTo.length?S("reply-to","reply-to",m.replyTo.join(", ")):""}\n ${S("date","date",new Date(i.createdAt).toLocaleString())}\n ${S("subject","subject",m?.subject??"")}\n ${S("mailed-by","mailed-by",`${ae[i.service]??i.service} (${i.source})`)}\n ${S("message-id","message id",i.providerId)}\n </div>`:""}\n ${m?.html?`<iframe class="mail-frame" name="email-body" ${l(r.emailBody)} data-format="html" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" title="Email body"></iframe>`:`<div class="mail-text" ${l(r.emailText)} data-format="text">${et(o(m?.text??""))}</div>`}\n ${m?.attachments.length?`<div class="attachments" ${l(r.emailAttachments)} data-count="${m.attachments.length}"><h3>${m.attachments.length} attachment${m.attachments.length===1?"":"s"}</h3><div class="att-grid">${m.attachments.map(C=>`<div class="att" ${l(r.emailAttachment)} data-filename="${o(C.filename)}" data-content-type="${o(C.contentType??"")}" data-size="${C.size??""}"><div class="att-top">${c.file()}</div><div class="att-name">${c.clip()} ${o(C.filename)}${C.size?` <span style="color:#5f6368;font-weight:400">${Math.max(1,Math.round(C.size/1024))} KB</span>`:""}</div></div>`).join("")}</div></div>`:""}\n <div class="reply-row"><span>${c.reply()} Reply</span><span>${c.forward()} Forward</span></div>\n </div>`}else f=`<div class="mail-toolbar"><span class="icon-btn">${c.checkbox()}</span><button class="icon-btn" data-action="refresh" ${l(r.emailRefresh)} title="Refresh" aria-label="Refresh">${c.refresh()}</button><span class="icon-btn">${c.more()}</span>\n <span class="range">${t.length?`1\\u2013${t.length} of ${t.length}`:""}</span></div>\n <div class="cat-tabs"><div class="on">${c.inbox()} Primary</div><div>${c.star()} Promotions</div><div>${c.send()} Updates</div></div>\n <div class="mail-list ${u?"enter":""}" role="list">${t.length?t.map(m=>{let x=!H.has(m.id),L=m.from.name??m.from.address,S=w.starred.has(m.id);return`<div class="swipe mrow-swipe" data-swipe="mrow" data-id="${o(m.id)}" data-flip="row:${o(m.id)}" role="listitem">\n <div class="swipe-actions">${c.archive()}${c.archive()}</div>\n <div class="mrow swipe-card ${x?"unread":""} ${a.arrived.has(m.id)?"arrive":""}" data-action="mail-open" data-id="${o(m.id)}" ${te(m)} ${l(r.emailRow)} data-starred="${S}" role="button" tabindex="0">\n <span class="chk" data-action="mail-check" data-id="${o(m.id)}" role="checkbox" aria-checked="${w.checked.has(m.id)}" aria-label="Select">${c.checkbox()}</span><span class="star" data-action="mail-star" data-id="${o(m.id)}" ${l(r.emailRowStar)} role="button" aria-pressed="${S}" aria-label="${S?"Starred":"Not starred"}">${S?c.starFilled():c.star()}</span>\n <span class="from" ${l(r.emailRowFrom)}>${o(L)}</span>\n <span class="subj"><span ${l(r.emailRowSubject)}>${o(m.email?.subject||"(no subject)")}</span> <span class="snippet">- <span ${l(r.emailRowSnippet)}>${o(m.preview)}</span></span></span>\n ${m.email?.attachments.length?`<span class="clip" ${l(r.emailRowAttachment)}>${c.clip()}</span>`:""}<span class="svc" ${l(r.emailRowService)}>${o(ae[m.service]??m.service)}</span>\n <span class="date" ${l(r.emailRowDate)}>${o(Pt(m.createdAt))}</span>\n <span class="hover-actions"><button class="icon-btn" data-action="mail-archive" data-id="${o(m.id)}" ${l(r.emailRowArchive)} title="Archive" aria-label="Archive">${c.archive()}</button><button class="icon-btn" data-action="mail-delete" data-id="${o(m.id)}" ${l(r.emailRowDelete)} title="Delete" aria-label="Delete">${c.trash()}</button><button class="icon-btn" data-action="mail-toggle-read" data-id="${o(m.id)}" ${l(r.emailRowToggleRead)} title="${x?"Mark as read":"Mark as unread"}" aria-label="${x?"Mark as read":"Mark as unread"}">${x?c.markRead():c.markUnread()}</button><span class="icon-btn">${c.clock()}</span></span></div></div>`}).join(""):`<div class="mail-empty" ${l(r.emailEmpty)}>${c.envelope()}<b>${e?"Your Primary tab is empty.":"No email yet"}</b>${e?"":"Email sent through Resend, SES, SNS, Customer.io or Mailosaur lands here."}</div>`}</div>`;let y=B("col-email");Ve(y,()=>{y.innerHTML=`\n <div class="device-head"><h2>Email ${je(n,r.emailUnreadCount)}</h2>${Re("email","Mailbox",r.emailRecipient,m=>m)}</div>\n <div class="window" ${l(r.emailDevice)} data-screen="${i?"message":"inbox"}" data-recipient="${o(e??"")}">\n <div class="chrome"><span class="lights"><i></i><i></i><i></i></span><span class="omnibox">${c.lock()} mail.example.test/#inbox${i?`/${o(i.providerId)}`:""}</span></div>\n <div class="mail-top"><span class="icon-btn">${c.menu()}</span><span class="mail-logo">${c.envelope()}<span class="lbl">Mail</span></span>\n <div class="mail-search"><span class="icon-btn">${c.search("#444746",20)}</span><span class="placeholder">Search mail</span><span class="icon-btn">${c.tune()}</span></div>\n <div class="right"><span class="icon-btn">${c.help()}</span><span class="icon-btn">${c.gear()}</span><span class="icon-btn">${c.apps()}</span>\n <span class="mail-avatar" title="${o(b)}" style="background:${xe(b)}">${o(b.slice(0,1).toUpperCase())}</span></div></div>\n <div class="mail-body">\n <nav class="mail-nav"><button class="compose">${c.pencil()}<span class="lbl">Compose</span></button>\n <div class="nav-item active">${c.inbox()}<span class="lbl">Inbox</span><span class="n">${s||""}</span></div>\n <div class="nav-item">${c.star()}<span class="lbl">Starred</span></div>\n <div class="nav-item">${c.clock()}<span class="lbl">Snoozed</span></div>\n <div class="nav-item">${c.send()}<span class="lbl">Sent</span></div>\n <div class="nav-item">${c.draft()}<span class="lbl">Drafts</span></div>\n <div class="nav-item">${c.expand()}<span class="lbl">More</span></div></nav>\n <main class="mail-main">${f}</main>\n </div>\n ${a.toast?`<div class="toast" ${l(r.toast)} role="status"><span>${o(a.toast.text)}</span>${a.toast.undo?`<button data-action="toast-undo" ${l(r.toastUndo)}>Undo</button>`:""}<button data-action="toast-close" aria-label="Dismiss">${c.close(16)}</button></div>`:""}\n </div>`});let g=y.querySelector(".mail-frame");if(g&&i&&(g.addEventListener("load",()=>{let m=g.contentDocument;m&&(g.style.height=`${Math.max(120,m.documentElement.scrollHeight+8)}px`)}),g.srcdoc=Zt(i)),k&&h){let m=fe.reduce((x,L)=>x+U(L).filter(S=>!H.has(S.id)).length,0);document.title=`${m?`(${m}) `:""}${rt()} \\xB7 Mockingbird`}},Jt=()=>{let e=it();B("timeline").innerHTML=e.length?`<table class="tl" ${l(r.timeline)}><thead><tr><th>Time</th><th>Channel</th><th>Source</th><th>To</th><th>From</th><th>Content</th><th>Status</th></tr></thead><tbody>${e.map(t=>`<tr data-action="inspect" data-id="${o(t.id)}" ${te(t)} ${l(r.timelineRow)} tabindex="0">\n <td class="mono" data-col="time">${o(It(t.createdAt))}</td>\n <td data-col="channel"><span class="chan ${t.channel}">${t.channel==="sms"?"SMS":t.channel==="email"?"Email":"Push"}</span></td>\n <td data-col="source"><span class="svc-tag">${o(ae[t.service]??t.service)}${t.source!==t.service?` \\xB7 ${o(t.source)}`:""}</span></td>\n <td class="mono" data-col="to">${o(t.channel==="sms"?Le(t.to):t.to.length>28?`${t.to.slice(0,26)}\\u2026`:t.to)}</td>\n <td data-col="from">${o(be(t))}</td>\n <td class="content" data-col="content">${t.email?`<b>${o(t.email.subject)}</b> \\u2014 `:t.push?.title?`<b>${o(t.push.title)}</b> \\u2014 `:""}${o(t.channel==="email"?t.preview.slice(0,120):t.sms?.body??t.push?.body??t.preview)}</td>\n <td class="svc-tag" data-col="status">${o(t.status??"")}</td></tr>`).join("")}</tbody></table>`:`<div class="empty-tl" ${l(r.timelineEmpty)}>Nothing sent in namespace <b>${o(a.namespace)}</b> yet.</div>`},ce=()=>{let e=a.items.find(u=>u.id===a.inspect),t=B("inspector");if(!e){t.hidden=!0,t.innerHTML="";for(let u of Object.keys(t.dataset))u!=="region"&&u!=="testid"&&delete t.dataset[u];return}t.hidden=!1,Object.assign(t.dataset,{notificationId:e.id,channel:e.channel,service:e.service,source:e.source,to:e.to,from:e.from.address,status:e.status??"",createdAt:e.createdAt});let{raw:s,...n}=e,i=(u,b,f)=>`<span>${b}</span><span ${l(r.inspectorField)} data-field="${u}">${o(f)}</span>`;t.innerHTML=`<header><h3 ${l(r.inspectorTitle)}>${o(e.channel.toUpperCase())} \\xB7 ${o(e.email?.subject??e.push?.title??e.preview.slice(0,60))}</h3>\n <button class="btn" data-action="show-on-device" data-id="${o(e.id)}" ${l(r.inspectorShowOnDevice)}>Show on device</button><button class="btn" data-action="close-inspector" ${l(r.inspectorClose)}>Close</button></header>\n <div class="kv">${i("id","Id",e.id)}${i("providerId","Provider id",e.providerId)}${i("service","Service",`${e.service} (${e.source})`)}\n ${i("namespace","Namespace",e.namespace)}${i("to","To",e.to)}${i("from","From",e.from.name?`${e.from.name} <${e.from.address}>`:e.from.address)}\n ${i("createdAt","Created",e.createdAt)}${i("status","Status",e.status??"\\u2014")}</div>\n <pre ${l(r.inspectorJson)}>${o(JSON.stringify({...n,raw:s},null,2))}</pre>`},pe=e=>(a.only===null||a.only===e)&&(p.channels===void 0||p.channels.includes(e)),F=()=>{if(!ne){X(),B("devices").hidden=a.view!=="devices",B("timeline").hidden=a.view!=="timeline";for(let e of fe)B(`col-${e}`).hidden=!pe(e);B("devices").classList.toggle("solo",fe.filter(pe).length===1),a.view==="devices"?(pe("sms")&&q(),pe("push")&&E(),pe("email")&&I()):Jt(),ce()}},Oe=e=>{a.view="devices",a[e.channel].to=e.to,e.channel==="sms"&&(a.sms.thread=e.from.address),e.channel==="email"&&(a.email.open=e.id),e.channel==="push"&&(a.push.expanded=e.id,w.openGroups.add(Ne(e.to,e.push?.app??"App"))),w.dismissed.delete(e.id),a.inspect=null,F(),d.querySelector(`.devices [data-notification-id="${CSS.escape(e.id)}"]`)?.scrollIntoView({block:"nearest"})},Qt=(e,t)=>{if(!W())return t();e.style.height=`${e.offsetHeight}px`,e.classList.add("collapsing"),e.offsetHeight,Object.assign(e.style,{height:"0px",marginTop:"0px",marginBottom:"0px",paddingTop:"0px",paddingBottom:"0px",opacity:"0"}),G(270,t)},Ue=(e,t,s,n)=>{if(!W())return n();e.classList.add("settling"),t.classList.add("settling"),t.style.transform=`translateX(${s*(e.offsetWidth+40)}px)`,t.style.opacity="0",G(230,()=>Qt(e,n))},$e=e=>d.querySelector(e),me=e=>e.querySelector(".swipe-card")??e,ye=(e,t=-1)=>{let s=$e(`.ios-swipe[data-id="${CSS.escape(e)}"]`)??$e(`.a-note[data-id="${CSS.escape(e)}"]`),n=()=>{w.dismissed.add(e),a.push.preview===e&&(a.push.preview=null),E()};if(!s)return n();Ue(s,me(s),t,n)},Be=e=>{let t=a.sms.to;if(!t)return;let s=U("sms").filter(u=>u.to===t&&u.from.address===e).map(u=>u.id),n=$e(`.thread-swipe[data-from="${CSS.escape(e)}"]`),i=()=>{for(let u of s)w.dismissed.add(u);q()};if(!n)return i();Ue(n,me(n),-1,i)},ke=(e,t,s=-1)=>{let n=$e(`.mrow-swipe[data-id="${CSS.escape(e)}"]`);if(!n)return ht(e,t);Ue(n,me(n),s,()=>ht(e,t))},M=null,ue=!1,ea=160,bt=148,gt=(e,t)=>{let s=me(e);s.style.transform=t?`translateX(${t}px)`:"",e.dataset.swipe==="a-note"&&(s.style.opacity=String(1-Math.min(.9,Math.abs(t)/e.offsetWidth)))},_=(e,t,s)=>{e.classList.add("settling"),e.classList.toggle("open",s),e.dataset.swipe==="a-note"&&e.classList.add("settling"),gt(e,t),G(340,()=>{e.classList.remove("settling","swiping"),e.dataset.offset=String(t)})},qe=e=>{let t=e.dataset.longpress,s=e.dataset.notificationId??e.dataset.id??"";if(t==="sms-bubble"){a.sms.menu=s,q();try{navigator.vibrate?.(10)}catch{}}else t==="ios-note"?(a.push.preview=s,E()):t==="a-note"&&(a.push.settings=a.push.settings===s?null:s,E())};P("pointerdown",e=>{if(K.unlock(),ue=!1,e.button!==0||!e.isPrimary)return;let t=e.target;if(t.closest("select, input, .swipe-actions button, .ios-overlay"))return;let s=t.closest("[data-swipe]"),n=t.closest("[data-longpress]");if(!(!s&&!n)&&(M={pointerId:e.pointerId,x0:e.clientX,y0:e.clientY,t0:performance.now(),dx:0,axis:null,swipe:s,press:n,base:Number(s?.dataset.offset??0),timer:null},n)){let i=M;i.timer=setTimeout(()=>{M!==i||i.axis||(ue=!0,M=null,qe(n))},480)}}),P("pointermove",e=>{if(!M||e.pointerId!==M.pointerId)return;let t=e.clientX-M.x0,s=e.clientY-M.y0;if(!M.axis){if(Math.hypot(t,s)<8)return;if(M.axis=Math.abs(t)>Math.abs(s)?"x":"y",M.timer&&clearTimeout(M.timer),M.axis==="x"&&M.swipe){M.swipe.classList.add("swiping"),M.swipe.classList.remove("settling"),me(M.swipe).classList.remove("settling");try{M.swipe.setPointerCapture(e.pointerId)}catch{}}}if(M.axis!=="x"||!M.swipe)return;e.preventDefault(),M.dx=t;let n=M.swipe.dataset.swipe,i=M.base+t;if(n==="sms-convo"){let u=Math.max(0,Math.min(72,-t));M.swipe.style.setProperty("--reveal",`${u}px`);return}n==="ios-note"&&i>0&&(i=i*.35),(n==="mrow"||n==="a-note")&&(i=t),gt(M.swipe,i)});let xt=e=>{if(!M||e.pointerId!==M.pointerId)return;let t=M;if(M=null,t.timer&&clearTimeout(t.timer),t.axis!=="x"||!t.swipe)return;ue=!0;let s=t.swipe,n=s.dataset.swipe,i=s.offsetWidth||1,u=t.dx/Math.max(1,performance.now()-t.t0),b=t.base+t.dx;if(n==="sms-convo")s.classList.add("settling"),s.style.setProperty("--reveal","0px"),G(320,()=>s.classList.remove("settling"));else if(n==="ios-note"){let f=s.dataset.id??"";b<-i*.7||u<-1.1&&b<-60?ye(f):b<-60?_(s,-ea,!0):b>70?(_(s,0,!1),a.push.expanded=f,E()):_(s,0,!1)}else if(n==="a-note")Math.abs(b)>i*.4||Math.abs(u)>.7?ye(s.dataset.id??"",Math.sign(b)||1):_(s,0,!1);else if(n==="sms-thread"){let f=s.dataset.from??"";b<-i*.6?Be(f):b<-50?_(s,-bt,!0):b>50?_(s,bt,!0):_(s,0,!1)}else n==="mrow"&&(Math.abs(b)>i*.35||Math.abs(u)>.8?ke(s.dataset.id??"","archived",Math.sign(b)||1):_(s,0,!1))};P("pointerup",xt),P("pointercancel",xt),P("contextmenu",e=>{let t=e.target.closest("[data-longpress]");t&&(e.preventDefault(),qe(t))}),P("click",e=>{ue&&(ue=!1,e.stopPropagation(),e.preventDefault())},!0);let vt=()=>{let e=a.sms.menu||a.push.preview||a.push.settings;return a.sms.menu=null,a.push.preview=null,a.push.settings=null,e&&(q(),E()),!!e},wt=e=>{try{navigator.clipboard?.writeText(e).catch(()=>{})}catch{}};P("click",e=>{let t=e.target.closest("[data-action]");if(!t)return;let s=t.closest(".swipe.open");if(s&&t.classList.contains("swipe-card")){_(s,0,!1);return}let n=t.dataset.action,i=t.dataset.id??"",u=t.dataset.from??"",b=a.sms.to??"";switch(n){case"view":a.view=t.dataset.value==="timeline"?"timeline":"devices",F();break;case"sound":Me.setSound(!K.enabled);break;case"sms-open":a.sms.thread=u||null,a.sms.transition="push",q();break;case"sms-back":a.sms.thread=null,a.sms.transition="back",q();break;case"thread-delete":Be(u);break;case"thread-pin":{let f=ie(b,u);w.pinned.delete(f)||w.pinned.add(f),q();break}case"thread-mute":{let f=ie(b,u);w.muted.delete(f)||w.muted.add(f),q();break}case"thread-unread":{let f=U("sms").filter(y=>y.to===b&&y.from.address===u);if(f.some(y=>!H.has(y.id)))le(f.map(y=>y.id));else{let y=f.at(-1);y&&ze([y.id])}q();break}case"tapback":{let f=t.dataset.value;w.tapbacks.get(i)===f?w.tapbacks.delete(i):w.tapbacks.set(i,f),a.sms.menu=null,q();break}case"sms-copy":{let f=a.items.find(y=>y.id===i);f&&wt(f.sms?.body??""),a.sms.menu=null,q();break}case"sms-copy-code":wt(t.dataset.code??"");break;case"close-overlay":vt();break;case"push-toggle":{if(t.dataset.groupTop==="true"){let f=a.items.find(y=>y.id===i);f&&(w.openGroups.add(Ne(f.to,f.push?.app??"App")),E());break}a.push.expanded=a.push.expanded===i?null:i,E();break}case"push-clear":a.push.preview=null,ye(i);break;case"push-options":a.push.preview=i,E();break;case"push-alerting":{let f=t.dataset.app??"App";t.dataset.value==="silent"?w.silent.add(f):w.silent.delete(f),E();break}case"push-clear-all":{let f=[...d.querySelectorAll(".shade-list .a-note")],y=f.map(m=>m.dataset.id??""),g=()=>{for(let m of y)w.dismissed.add(m);E()};if(!W())return g();f.forEach((m,x)=>{G(x*60,()=>{m.classList.add("settling"),m.style.transform=`translateX(${m.offsetWidth+40}px)`,m.style.opacity="0"})}),G(f.length*60+300,g);break}case"group-open":w.openGroups.add(t.dataset.key??""),E();break;case"group-collapse":w.openGroups.delete(t.dataset.key??""),E();break;case"group-clear":{let f=t.dataset.app??"App";for(let y of U("push"))y.to===a.push.to&&(y.push?.app??"App")===f&&w.dismissed.add(y.id);w.openGroups.delete(t.dataset.key??""),E();break}case"skin":a.push.skin=t.dataset.value??"auto",E();break;case"mail-open":a.email.open=i,a.email.transition=!0,re=!1,I();break;case"mail-back":a.email.open=null,a.email.transition=!0,I();break;case"mail-details":re=!re,I();break;case"mail-archive":ke(i,"archived");break;case"mail-delete":ke(i,"deleted");break;case"mail-unread":ze([i]),a.email.open=null,a.email.transition=!0,I();break;case"mail-toggle-read":H.has(i)?ze([i]):le([i]),I();break;case"mail-star":w.starred.delete(i)||w.starred.add(i),I();break;case"mail-check":w.checked.delete(i)||w.checked.add(i),I();break;case"toast-undo":a.toast?.undo?.();break;case"toast-close":a.toast=null,I();break;case"refresh":ve().catch(()=>{});break;case"inspect":a.inspect=i,ce();break;case"close-inspector":a.inspect=null,ce();break;case"show-on-device":{let f=a.items.find(y=>y.id===i);f&&Oe(f);break}}}),P("dblclick",e=>{let t=e.target.closest("[data-notification-id]");!t||t.closest("[data-region=inspector]")||(a.inspect=t.dataset.notificationId??null,ce())}),P("change",e=>{let t=e.target,s=t.dataset.action;if(s==="namespace")Me.setNamespace(t.value);else if(s==="follow")a.follow=t.checked;else if(s==="recipient"){let n=t.dataset.channel;a[n].to=t.value,n==="sms"&&(a.sms.thread=null),n==="email"&&(a.email.open=null),F()}}),P("input",e=>{let t=e.target;t.dataset.action==="search"&&(a.search=t.value,Ce(),F())}),P("keydown",e=>{K.unlock();let t=e.target;if(e.key==="Escape"){if(vt())return;a.inspect&&(a.inspect=null,ce());return}if(!t.matches("input, select, textarea")){if(e.key==="ContextMenu"||e.shiftKey&&e.key==="F10"){let s=t.closest("[data-longpress]");s&&(e.preventDefault(),qe(s));return}if(e.key==="Delete"||e.key==="Backspace"||e.key==="#"||e.key==="e"){let s=t.dataset.id??"";t.matches("[data-testid=push-notification]")&&e.key!=="e"?(e.preventDefault(),ye(s)):t.matches("[data-testid=sms-thread]")&&e.key!=="e"?(e.preventDefault(),Be(t.dataset.from??"")):t.matches("[data-testid=email-row]")&&(e.preventDefault(),ke(s,e.key==="e"?"archived":"deleted"));return}(e.key==="Enter"||e.key===" ")&&t.matches("[role=button]:not(button)")&&(e.preventDefault(),t.click())}});let $t=()=>{if(O&&(a[O.channel].to=O.to,O.channel==="sms"&&(a.sms.thread=Ht??Ie(O.to)[0]?.from??null)),_e){let e=a.items.find(t=>t.id===_e);if(e)return Oe(e)}F()},Fe=()=>{},ta=new Promise(e=>{Fe=()=>{d.dataset.ready="true",e()}}),Me={render:F,state:a,ready:ta,show(e){let t=e.id?a.items.find(s=>s.id===e.id):void 0;if(t)return Oe(t);e.channel&&(a.view="devices",e.to&&(a[e.channel].to=e.to),e.channel==="sms"&&e.to&&(a.sms.thread=Ie(e.to)[0]?.from??null),F())},setNamespace(e){if(e!==a.namespace){if(a.namespace=e,a.items=[],a.sms={...a.sms,to:null,thread:null,menu:null},a.email={...a.email,to:null,open:null},a.push.to=null,Je(),k){let t=new URL(location.href);t.searchParams.set("namespace",e),history.replaceState(null,"",t)}h?(ot(),ve().catch(()=>{})):F()}},setSound(e){if(K.enabled=e,e&&K.unlock(),T)try{localStorage.setItem(Ze,e?"1":"0")}catch{}X()},destroy(){ne=!0,D.abort(),Ee?.abort();for(let e of he)clearTimeout(e);he.clear();for(let e of yt)clearInterval(e);K.close(),d.innerHTML="",d.classList.remove(Te,"embedded")}},yt=[];return h?(yt.push(setInterval(()=>{M||a.sms.menu||a.push.preview||a.push.settings||(a.view==="devices"&&!d.querySelector(".mail-frame")?F():X())},3e4),setInterval(()=>{st()},5e3)),Je(),F(),ot(),st(),(async()=>{for(let e=0;!ne;e++)try{await ve(),$t(),Fe();return}catch{await new Promise(t=>G(Math.min(5e3,500*2**e),()=>t(null)))}})(),Me):(a.items=[...p.items??[]].sort((e,t)=>e.createdAt<t.createdAt?1:-1),a.sources=p.sources??[],a.namespaces=p.namespaces??[],a.discoveryError=p.discoveryError??null,a.connected=p.connected??!0,Ce(),$t(),Fe(),Me)}At(document.getElementById("app"),{live:!0});\n';
|
|
244
|
+
|
|
245
|
+
// src/sources.ts
|
|
246
|
+
var isRow = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
247
|
+
var row = (value) => isRow(value) ? value : {};
|
|
248
|
+
var str = (value) => typeof value === "string" ? value : typeof value === "number" ? String(value) : null;
|
|
249
|
+
var strings = (value) => Array.isArray(value) ? value.map(str).filter((v) => v !== null && v !== "") : typeof value === "string" && value !== "" ? [value] : [];
|
|
250
|
+
var list = (value, key) => {
|
|
251
|
+
const items = isRow(value) ? value[key] : value;
|
|
252
|
+
return Array.isArray(items) ? items.filter(isRow) : [];
|
|
253
|
+
};
|
|
254
|
+
var iso = (value) => {
|
|
255
|
+
if (typeof value === "number") return new Date(value < 1e11 ? value * 1e3 : value).toISOString();
|
|
256
|
+
if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
|
|
257
|
+
return new Date(value).toISOString();
|
|
258
|
+
return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
259
|
+
};
|
|
260
|
+
var parseAddress = (value) => {
|
|
261
|
+
const match = /^\s*"?([^"<]*?)"?\s*<([^>]+)>\s*$/.exec(value);
|
|
262
|
+
if (match) return { name: match[1]?.trim() || null, address: (match[2] ?? "").trim() };
|
|
263
|
+
return { name: null, address: value.trim() };
|
|
264
|
+
};
|
|
265
|
+
var bare = (value) => parseAddress(value).address.toLowerCase();
|
|
266
|
+
var stripHtml = (html) => html.replace(/<(style|script|head)[^>]*>[\s\S]*?<\/\1>/gi, " ").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/[ \t]+/g, " ").replace(/\s*\n\s*/g, "\n").trim();
|
|
267
|
+
var preview = (text) => text.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
268
|
+
var base = (input, channel) => ({
|
|
269
|
+
id: `${input.context.source}:${input.context.namespace}:${input.providerId}${channel === "email" ? `#${input.to.toLowerCase()}` : ""}`,
|
|
270
|
+
providerId: input.providerId,
|
|
271
|
+
channel,
|
|
272
|
+
source: input.context.source,
|
|
273
|
+
service: input.service,
|
|
274
|
+
namespace: input.context.namespace,
|
|
275
|
+
to: input.to,
|
|
276
|
+
from: input.from,
|
|
277
|
+
createdAt: input.createdAt,
|
|
278
|
+
status: input.status ?? null,
|
|
279
|
+
raw: input.raw
|
|
280
|
+
});
|
|
281
|
+
var smsNotification = (input, sms) => ({
|
|
282
|
+
...base(input, "sms"),
|
|
283
|
+
preview: preview(sms.body),
|
|
284
|
+
sms: { body: sms.body, code: sms.code ?? null, media: sms.media ?? [] },
|
|
285
|
+
email: null,
|
|
286
|
+
push: null
|
|
287
|
+
});
|
|
288
|
+
var emailNotifications = (input, recipients, email) => {
|
|
289
|
+
const content = {
|
|
290
|
+
subject: email.subject,
|
|
291
|
+
html: email.html ?? null,
|
|
292
|
+
text: email.text ?? null,
|
|
293
|
+
cc: email.cc ?? [],
|
|
294
|
+
bcc: email.bcc ?? [],
|
|
295
|
+
replyTo: email.replyTo ?? [],
|
|
296
|
+
attachments: email.attachments ?? [],
|
|
297
|
+
headers: email.headers ?? {}
|
|
298
|
+
};
|
|
299
|
+
const body = content.text ?? (content.html ? stripHtml(content.html) : "");
|
|
300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
301
|
+
return recipients.flatMap((recipient) => {
|
|
302
|
+
const to = bare(recipient);
|
|
303
|
+
if (!to || seen.has(to)) return [];
|
|
304
|
+
seen.add(to);
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
...base({ ...input, to }, "email"),
|
|
308
|
+
preview: preview(body),
|
|
309
|
+
sms: null,
|
|
310
|
+
email: content,
|
|
311
|
+
push: null
|
|
312
|
+
}
|
|
313
|
+
];
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
var pushNotification = (input, push) => ({
|
|
317
|
+
...base(input, "push"),
|
|
318
|
+
preview: preview([push.title, push.body].filter(Boolean).join(" \u2014 ")),
|
|
319
|
+
sms: null,
|
|
320
|
+
email: null,
|
|
321
|
+
push
|
|
322
|
+
});
|
|
323
|
+
var twilio = async (context) => {
|
|
324
|
+
const messages = list(await context.read("/outbox"), "messages");
|
|
325
|
+
return messages.flatMap((m) => {
|
|
326
|
+
const providerId = str(m.id) ?? str(m.sid) ?? "";
|
|
327
|
+
const body = str(m.body) ?? "";
|
|
328
|
+
const to = str(m.to) ?? "";
|
|
329
|
+
const verify = m.kind === "verify";
|
|
330
|
+
const sender = str(m.from) ?? str(m.messagingServiceSid) ?? (verify ? "Verify" : "Twilio");
|
|
331
|
+
const common = {
|
|
332
|
+
context,
|
|
333
|
+
service: "twilio",
|
|
334
|
+
providerId,
|
|
335
|
+
from: { name: verify && !m.from ? "Verify" : null, address: sender },
|
|
336
|
+
createdAt: iso(m.createdAt),
|
|
337
|
+
status: str(m.status),
|
|
338
|
+
raw: m
|
|
339
|
+
};
|
|
340
|
+
if (m.channel === "email")
|
|
341
|
+
return emailNotifications(common, [to], {
|
|
342
|
+
subject: "Your verification code",
|
|
343
|
+
text: body
|
|
344
|
+
});
|
|
345
|
+
return [smsNotification({ ...common, to }, { body, code: str(m.code) })];
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
var resend = async (context) => list(await context.read("/outbox"), "messages").flatMap((m) => {
|
|
349
|
+
const cc = strings(m.cc);
|
|
350
|
+
const bcc = strings(m.bcc);
|
|
351
|
+
return emailNotifications(
|
|
352
|
+
{
|
|
353
|
+
context,
|
|
354
|
+
service: "resend",
|
|
355
|
+
providerId: str(m.id) ?? "",
|
|
356
|
+
from: parseAddress(str(m.from) ?? ""),
|
|
357
|
+
createdAt: iso(m.createdAt),
|
|
358
|
+
status: m.scheduledAt ? "scheduled" : "delivered",
|
|
359
|
+
raw: m
|
|
360
|
+
},
|
|
361
|
+
[...strings(m.to), ...cc, ...bcc],
|
|
362
|
+
{
|
|
363
|
+
subject: str(m.subject) ?? "",
|
|
364
|
+
html: str(m.html),
|
|
365
|
+
text: str(m.text),
|
|
366
|
+
cc,
|
|
367
|
+
bcc,
|
|
368
|
+
replyTo: strings(m.replyTo),
|
|
369
|
+
headers: Object.fromEntries(Object.entries(row(m.headers)).map(([k, v]) => [k, String(v)])),
|
|
370
|
+
attachments: list(m.attachments, "").map((a) => ({
|
|
371
|
+
filename: str(a.filename) ?? "attachment",
|
|
372
|
+
contentType: str(a.contentType),
|
|
373
|
+
size: typeof a.size === "number" ? a.size : null
|
|
374
|
+
}))
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
var platformOf = (value) => value === "android" || value === "ios" || value === "web" ? value : "unknown";
|
|
379
|
+
var fcm = async (context) => list(await context.read("/outbox"), "messages").map((m) => {
|
|
380
|
+
const wire = row(m.wire);
|
|
381
|
+
const notification = row(m.notification ?? wire.notification);
|
|
382
|
+
const android = row(row(m.android ?? wire.android).notification);
|
|
383
|
+
const aps = row(row(row(m.apns ?? wire.apns).payload).aps);
|
|
384
|
+
const alert = isRow(aps.alert) ? aps.alert : { body: aps.alert };
|
|
385
|
+
const webpush = row(row(wire.webpush).notification);
|
|
386
|
+
const platform = platformOf(m.platform);
|
|
387
|
+
const pick = (key) => (platform === "android" ? str(android[key]) : null) ?? (platform === "ios" ? str(alert[key]) : null) ?? (platform === "web" ? str(webpush[key]) : null) ?? str(notification[key]);
|
|
388
|
+
const image = (platform === "android" ? str(android.image) : null) ?? str(row(row(m.apns ?? wire.apns).fcm_options).image) ?? str(notification.image);
|
|
389
|
+
const project = str(m.project) ?? "app";
|
|
390
|
+
return pushNotification(
|
|
391
|
+
{
|
|
392
|
+
context,
|
|
393
|
+
service: "fcm",
|
|
394
|
+
providerId: str(m.id) ?? str(m.name) ?? "",
|
|
395
|
+
to: str(m.token) ?? "",
|
|
396
|
+
from: { name: project, address: str(m.name) ?? project },
|
|
397
|
+
createdAt: iso(m.acceptedAt),
|
|
398
|
+
status: str(m.state),
|
|
399
|
+
raw: m
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
title: pick("title"),
|
|
403
|
+
body: pick("body"),
|
|
404
|
+
imageUrl: image,
|
|
405
|
+
app: project,
|
|
406
|
+
platform,
|
|
407
|
+
data: Object.fromEntries(
|
|
408
|
+
Object.entries(row(m.data ?? wire.data)).map(([k, v]) => [k, String(v)])
|
|
409
|
+
),
|
|
410
|
+
badge: typeof aps.badge === "number" ? aps.badge : null,
|
|
411
|
+
sound: str(aps.sound) ?? str(android.sound)
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
var stateRecords = async (read, collection) => {
|
|
416
|
+
const out = [];
|
|
417
|
+
let after = null;
|
|
418
|
+
for (let page = 0; page < 100; page++) {
|
|
419
|
+
const query = after === null ? "" : `&after=${encodeURIComponent(String(after))}`;
|
|
420
|
+
const result = row(await read(`/state/${collection}?limit=200&order=oldest${query}`));
|
|
421
|
+
for (const record of list(result, "records")) out.push(record);
|
|
422
|
+
if (result.next === null || result.next === void 0) break;
|
|
423
|
+
after = result.next;
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
};
|
|
427
|
+
var ses = async (context) => (await stateRecords(context.read, "ses_messages")).flatMap((record) => {
|
|
428
|
+
const v = row(record.value);
|
|
429
|
+
const destination = row(v.Destination);
|
|
430
|
+
const common = {
|
|
431
|
+
context,
|
|
432
|
+
service: "ses",
|
|
433
|
+
providerId: str(v.MessageId) ?? str(record.id) ?? "",
|
|
434
|
+
createdAt: iso(v.Timestamp),
|
|
435
|
+
status: "delivered",
|
|
436
|
+
raw: v
|
|
437
|
+
};
|
|
438
|
+
const raw = row(v.RawMessage);
|
|
439
|
+
if (raw.Data !== void 0) {
|
|
440
|
+
const mime = parseMime(decodeRawMessage(str(raw.Data) ?? ""));
|
|
441
|
+
const recipients = strings(v.Destinations);
|
|
442
|
+
return emailNotifications(
|
|
443
|
+
{ ...common, from: parseAddress(str(v.Source) ?? mime.from ?? "") },
|
|
444
|
+
recipients.length ? recipients : [...mime.to, ...mime.cc],
|
|
445
|
+
{
|
|
446
|
+
subject: mime.subject,
|
|
447
|
+
html: mime.html,
|
|
448
|
+
text: mime.text,
|
|
449
|
+
cc: mime.cc,
|
|
450
|
+
attachments: mime.attachments,
|
|
451
|
+
headers: mime.headers,
|
|
452
|
+
replyTo: addressList(mime.headers["reply-to"])
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const message = row(v.Message);
|
|
457
|
+
const body = row(message.Body);
|
|
458
|
+
const cc = strings(destination.CcAddresses);
|
|
459
|
+
const bcc = strings(destination.BccAddresses);
|
|
460
|
+
return emailNotifications(
|
|
461
|
+
{ ...common, from: parseAddress(str(v.Source) ?? "") },
|
|
462
|
+
[...strings(destination.ToAddresses), ...cc, ...bcc],
|
|
463
|
+
{
|
|
464
|
+
subject: str(row(message.Subject).Data) ?? "",
|
|
465
|
+
html: str(row(body.Html).Data),
|
|
466
|
+
text: str(row(body.Text).Data),
|
|
467
|
+
cc,
|
|
468
|
+
bcc,
|
|
469
|
+
replyTo: strings(v.ReplyToAddresses)
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
var snsBody = (v, protocol) => {
|
|
474
|
+
const message = str(v.Message) ?? "";
|
|
475
|
+
if (v.MessageStructure !== "json") return message;
|
|
476
|
+
try {
|
|
477
|
+
const parsed = row(JSON.parse(message));
|
|
478
|
+
return str(parsed[protocol]) ?? str(parsed.default) ?? message;
|
|
479
|
+
} catch {
|
|
480
|
+
return message;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var sns = async (context) => {
|
|
484
|
+
const [messages, subscriptions] = await Promise.all([
|
|
485
|
+
stateRecords(context.read, "sns_messages"),
|
|
486
|
+
stateRecords(context.read, "sns_subscriptions")
|
|
487
|
+
]);
|
|
488
|
+
return messages.flatMap((record) => {
|
|
489
|
+
const v = row(record.value);
|
|
490
|
+
const providerId = str(v.MessageId) ?? str(record.id) ?? "";
|
|
491
|
+
const common = { context, service: "sns", createdAt: iso(v.Timestamp), raw: v };
|
|
492
|
+
const phone = str(v.PhoneNumber);
|
|
493
|
+
if (phone)
|
|
494
|
+
return [
|
|
495
|
+
smsNotification(
|
|
496
|
+
{
|
|
497
|
+
...common,
|
|
498
|
+
providerId,
|
|
499
|
+
to: phone,
|
|
500
|
+
from: { name: null, address: "SNS" },
|
|
501
|
+
status: "delivered"
|
|
502
|
+
},
|
|
503
|
+
{ body: snsBody(v, "sms") }
|
|
504
|
+
)
|
|
505
|
+
];
|
|
506
|
+
const topicArn = str(v.TopicArn) ?? "";
|
|
507
|
+
const topicName = topicArn.split(":").at(-1) ?? "SNS";
|
|
508
|
+
return subscriptions.flatMap((sub) => {
|
|
509
|
+
const s = row(sub.value);
|
|
510
|
+
if (s.TopicArn !== topicArn) return [];
|
|
511
|
+
const endpoint = str(s.Endpoint) ?? "";
|
|
512
|
+
const subId = `${providerId}@${str(s.SubscriptionArn) ?? endpoint}`;
|
|
513
|
+
if (s.Protocol === "sms")
|
|
514
|
+
return [
|
|
515
|
+
smsNotification(
|
|
516
|
+
{
|
|
517
|
+
...common,
|
|
518
|
+
providerId: subId,
|
|
519
|
+
to: endpoint,
|
|
520
|
+
from: { name: topicName, address: topicName },
|
|
521
|
+
status: "delivered"
|
|
522
|
+
},
|
|
523
|
+
{ body: snsBody(v, "sms") }
|
|
524
|
+
)
|
|
525
|
+
];
|
|
526
|
+
if (s.Protocol === "email" || s.Protocol === "email-json") {
|
|
527
|
+
const text = s.Protocol === "email-json" ? JSON.stringify(
|
|
528
|
+
{
|
|
529
|
+
Type: "Notification",
|
|
530
|
+
MessageId: providerId,
|
|
531
|
+
TopicArn: topicArn,
|
|
532
|
+
Subject: v.Subject ?? null,
|
|
533
|
+
Message: snsBody(v, "email-json")
|
|
534
|
+
},
|
|
535
|
+
null,
|
|
536
|
+
2
|
|
537
|
+
) : `${snsBody(v, "email")}
|
|
538
|
+
|
|
539
|
+
--
|
|
540
|
+
If you wish to stop receiving notifications from this topic, please unsubscribe.
|
|
541
|
+
|
|
542
|
+
Please do not reply directly to this email. If you have any questions or comments regarding this email, please contact us at https://aws.amazon.com/support`;
|
|
543
|
+
return emailNotifications(
|
|
544
|
+
{
|
|
545
|
+
...common,
|
|
546
|
+
providerId: subId,
|
|
547
|
+
from: { name: "AWS Notifications", address: "no-reply@sns.amazonaws.com" },
|
|
548
|
+
status: "delivered"
|
|
549
|
+
},
|
|
550
|
+
[endpoint],
|
|
551
|
+
{ subject: str(v.Subject) ?? "AWS Notification Message", text }
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
return [];
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
var escapeHtml = (value) => value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
559
|
+
var templatePreview = (m) => {
|
|
560
|
+
const data = Object.entries(row(m.messageData));
|
|
561
|
+
const rows = data.map(
|
|
562
|
+
([k, v]) => `<tr><td style="padding:6px 12px;color:#6b7280;font:13px system-ui">${escapeHtml(k)}</td><td style="padding:6px 12px;font:13px ui-monospace,monospace">${escapeHtml(typeof v === "string" ? v : JSON.stringify(v))}</td></tr>`
|
|
563
|
+
).join("");
|
|
564
|
+
const links = strings(m.links).map(
|
|
565
|
+
(href) => `<p style="font:13px system-ui"><a href="${escapeHtml(href)}">${escapeHtml(href)}</a></p>`
|
|
566
|
+
).join("");
|
|
567
|
+
return `<div style="font:14px system-ui;color:#111827;padding:8px"><p style="margin:0 0 4px;color:#6b7280;font-size:12px">Customer.io transactional message <b>${escapeHtml(str(m.transactionalMessageId) ?? "")}</b></p><p style="margin:0 0 12px;color:#6b7280;font-size:12px">The template body is rendered by Customer.io; these are the <code>message_data</code> values it was sent with.</p>${rows ? `<table style="border-collapse:collapse;border:1px solid #e5e7eb;border-radius:8px">${rows}</table>` : "<p><i>No message_data.</i></p>"}${links}</div>`;
|
|
568
|
+
};
|
|
569
|
+
var customerio = async (context) => list(await context.read("/outbox"), "messages").flatMap((m) => {
|
|
570
|
+
const identifiers = row(m.identifiers);
|
|
571
|
+
const to = str(m.to) ?? str(identifiers.email) ?? str(identifiers.id) ?? "";
|
|
572
|
+
const state = str(m.state);
|
|
573
|
+
const common = {
|
|
574
|
+
context,
|
|
575
|
+
service: "customerio",
|
|
576
|
+
providerId: str(m.id) ?? "",
|
|
577
|
+
createdAt: iso(m.createdAt),
|
|
578
|
+
status: state,
|
|
579
|
+
raw: m
|
|
580
|
+
};
|
|
581
|
+
const templateId = str(m.transactionalMessageId) ?? "message";
|
|
582
|
+
if (m.channel === "sms") {
|
|
583
|
+
const data = row(m.messageData);
|
|
584
|
+
const body = str(data.body) ?? str(data.message) ?? str(data.text) ?? `[${templateId}] ${Object.entries(data).map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ")}`.trim();
|
|
585
|
+
return [
|
|
586
|
+
smsNotification(
|
|
587
|
+
{ ...common, to, from: { name: null, address: str(m.from) ?? "Customer.io" } },
|
|
588
|
+
{ body }
|
|
589
|
+
)
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
if (m.channel === "email")
|
|
593
|
+
return emailNotifications(
|
|
594
|
+
{ ...common, from: parseAddress(str(m.from) ?? "Customer.io <no-reply@customer.io>") },
|
|
595
|
+
[to],
|
|
596
|
+
{
|
|
597
|
+
subject: str(m.subject) ?? `Transactional message ${templateId}`,
|
|
598
|
+
html: templatePreview(m),
|
|
599
|
+
attachments: strings(m.attachments).map((filename) => ({
|
|
600
|
+
filename,
|
|
601
|
+
contentType: null,
|
|
602
|
+
size: null
|
|
603
|
+
}))
|
|
604
|
+
}
|
|
605
|
+
);
|
|
606
|
+
return [];
|
|
607
|
+
});
|
|
608
|
+
var mailosaurAddress = (value) => {
|
|
609
|
+
const a = row(value);
|
|
610
|
+
return str(a.email) ?? str(a.phone) ?? str(a.phoneNumber) ?? "";
|
|
611
|
+
};
|
|
612
|
+
var mailosaur = async (context) => list(await context.read("/outbox"), "messages").flatMap((entry) => {
|
|
613
|
+
const m = row(entry.message);
|
|
614
|
+
const from = row(list(m.from, "")[0]);
|
|
615
|
+
const recipients = list(m.to, "").map(mailosaurAddress).filter(Boolean);
|
|
616
|
+
const common = {
|
|
617
|
+
context,
|
|
618
|
+
service: "mailosaur",
|
|
619
|
+
providerId: str(entry.id) ?? str(m.id) ?? "",
|
|
620
|
+
createdAt: iso(m.received ?? entry.createdAt),
|
|
621
|
+
status: "received",
|
|
622
|
+
raw: entry
|
|
623
|
+
};
|
|
624
|
+
const html = str(row(m.html).body);
|
|
625
|
+
const text = str(row(m.text).body);
|
|
626
|
+
if (m.type === "SMS")
|
|
627
|
+
return recipients.map(
|
|
628
|
+
(to) => smsNotification(
|
|
629
|
+
{
|
|
630
|
+
...common,
|
|
631
|
+
to,
|
|
632
|
+
from: { name: str(from.name) || null, address: mailosaurAddress(from) }
|
|
633
|
+
},
|
|
634
|
+
{ body: text ?? (html ? stripHtml(html) : "") }
|
|
635
|
+
)
|
|
636
|
+
);
|
|
637
|
+
return emailNotifications(
|
|
638
|
+
{ ...common, from: { name: str(from.name) || null, address: mailosaurAddress(from) } },
|
|
639
|
+
[...recipients, ...list(m.cc, "").map(mailosaurAddress)],
|
|
640
|
+
{
|
|
641
|
+
subject: str(m.subject) ?? "",
|
|
642
|
+
html,
|
|
643
|
+
text,
|
|
644
|
+
cc: list(m.cc, "").map(mailosaurAddress),
|
|
645
|
+
attachments: list(m.attachments, "").map((a) => ({
|
|
646
|
+
filename: str(a.fileName) ?? str(a.filename) ?? "attachment",
|
|
647
|
+
contentType: str(a.contentType),
|
|
648
|
+
size: typeof a.length === "number" ? a.length : null
|
|
649
|
+
}))
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
var READERS = {
|
|
654
|
+
twilio,
|
|
655
|
+
resend,
|
|
656
|
+
fcm,
|
|
657
|
+
ses,
|
|
658
|
+
sns,
|
|
659
|
+
customerio,
|
|
660
|
+
mailosaur
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/ui/page.ts
|
|
664
|
+
var escapeHtml2 = (value) => value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
665
|
+
var renderPage = ({ title }) => `<!doctype html>
|
|
666
|
+
<html lang="en">
|
|
667
|
+
<head>
|
|
668
|
+
<meta charset="utf-8">
|
|
669
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
670
|
+
<meta name="color-scheme" content="dark">
|
|
671
|
+
<title>${escapeHtml2(title)} \xB7 Mockingbird</title>
|
|
672
|
+
<link rel="icon" href="data:image/svg+xml,${encodeURIComponent(
|
|
673
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#0a84ff"/><path d="M9 13a7 7 0 0 1 14 0v5l2 3H7l2-3z" fill="#fff"/><circle cx="16" cy="24.5" r="2.5" fill="#fff"/></svg>'
|
|
674
|
+
)}">
|
|
675
|
+
<style>${PAGE_STYLES}${STYLES}</style>
|
|
676
|
+
</head>
|
|
677
|
+
<body>
|
|
678
|
+
<div id="app" data-title="${escapeHtml2(title)}"><div class="boot">Loading notifications\u2026</div></div>
|
|
679
|
+
<script type="module" src="./ui.js"></script>
|
|
680
|
+
</body>
|
|
681
|
+
</html>`;
|
|
682
|
+
|
|
683
|
+
// src/harness.ts
|
|
684
|
+
var json = (status, body, headers = {}) => new Response(JSON.stringify(body, null, 2), {
|
|
685
|
+
status,
|
|
686
|
+
headers: { "content-type": "application/json; charset=utf-8", ...headers }
|
|
687
|
+
});
|
|
688
|
+
var errorBody = (status, message) => json(status, { error: { type: "mockingbird_notifications", message } });
|
|
689
|
+
var REDISCOVER_MS = 2e3;
|
|
690
|
+
var DUPLICATE_WINDOW_MS = 1e4;
|
|
691
|
+
var fingerprint = (n) => [
|
|
692
|
+
n.channel,
|
|
693
|
+
n.namespace,
|
|
694
|
+
n.to.toLowerCase(),
|
|
695
|
+
n.from.address.toLowerCase(),
|
|
696
|
+
n.email?.subject ?? "",
|
|
697
|
+
n.preview
|
|
698
|
+
].join("\0");
|
|
699
|
+
var dedupe = (items) => {
|
|
700
|
+
const senders = /* @__PURE__ */ new Map();
|
|
701
|
+
for (const n of items)
|
|
702
|
+
if (n.service !== "mailosaur") {
|
|
703
|
+
const key = fingerprint(n);
|
|
704
|
+
senders.set(key, [...senders.get(key) ?? [], Date.parse(n.createdAt)]);
|
|
705
|
+
}
|
|
706
|
+
return items.filter((n) => {
|
|
707
|
+
if (n.service !== "mailosaur") return true;
|
|
708
|
+
const at = Date.parse(n.createdAt);
|
|
709
|
+
return !(senders.get(fingerprint(n)) ?? []).some((t) => Math.abs(t - at) <= DUPLICATE_WINDOW_MS);
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
var byTime = (a, b) => a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt < b.createdAt ? -1 : 1;
|
|
713
|
+
var createNotificationHarness = (options = {}) => {
|
|
714
|
+
if (!options.fleet && !options.manifest && !options.services)
|
|
715
|
+
throw new Error("createNotificationHarness: pass fleet, manifest or services");
|
|
716
|
+
const fetchImpl = options.fetch ?? ((request) => fetch(request));
|
|
717
|
+
const authHeaders = options.adminKey ? { "x-mockingbird-admin-key": options.adminKey } : {};
|
|
718
|
+
const followAll = (options.namespaces ?? (options.fleet ? "all" : void 0)) === "all";
|
|
719
|
+
const watched = new Set(
|
|
720
|
+
Array.isArray(options.namespaces) ? options.namespaces : ["default"]
|
|
721
|
+
);
|
|
722
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
723
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
724
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
725
|
+
const explicit = options.services ? endpointsFromServices(options.services, fetchImpl) : [];
|
|
726
|
+
let endpoints = explicit;
|
|
727
|
+
let discoveredAt = 0;
|
|
728
|
+
let discoveryError = null;
|
|
729
|
+
let discovering = null;
|
|
730
|
+
const rejected = /* @__PURE__ */ new Set();
|
|
731
|
+
const rediscover = async (force = false) => {
|
|
732
|
+
if (!options.fleet && !options.manifest) return;
|
|
733
|
+
if (!force && Date.now() - discoveredAt < REDISCOVER_MS) return;
|
|
734
|
+
discovering ??= (async () => {
|
|
735
|
+
try {
|
|
736
|
+
let found = [];
|
|
737
|
+
if (options.fleet) {
|
|
738
|
+
const fleet = await discoverFleet(options.fleet, fetchImpl, authHeaders);
|
|
739
|
+
found = fleet.endpoints;
|
|
740
|
+
if (followAll) for (const ns of fleet.namespaces) watched.add(ns);
|
|
741
|
+
} else if (options.manifest) {
|
|
742
|
+
const manifest = typeof options.manifest === "function" ? await options.manifest() : options.manifest;
|
|
743
|
+
found = endpointsFromManifest(manifest, fetchImpl);
|
|
744
|
+
}
|
|
745
|
+
const known = new Map(endpoints.map((e) => [`${e.name}|${e.adminUrl}`, e.service]));
|
|
746
|
+
endpoints = [
|
|
747
|
+
...explicit,
|
|
748
|
+
...found.filter(
|
|
749
|
+
(e) => !explicit.some((x) => x.name === e.name) && !rejected.has(`${e.name}|${e.adminUrl}`)
|
|
750
|
+
).map((e) => ({
|
|
751
|
+
...e,
|
|
752
|
+
service: e.service ?? known.get(`${e.name}|${e.adminUrl}`) ?? null
|
|
753
|
+
}))
|
|
754
|
+
];
|
|
755
|
+
discoveryError = null;
|
|
756
|
+
} catch (error) {
|
|
757
|
+
discoveryError = error instanceof Error ? error.message : String(error);
|
|
758
|
+
} finally {
|
|
759
|
+
discoveredAt = Date.now();
|
|
760
|
+
discovering = null;
|
|
761
|
+
}
|
|
762
|
+
})();
|
|
763
|
+
await discovering;
|
|
764
|
+
};
|
|
765
|
+
const adminGet = (endpoint, namespace) => async (path) => {
|
|
766
|
+
const response = await endpoint.fetch(
|
|
767
|
+
new Request(`${endpoint.adminUrl}${path}`, {
|
|
768
|
+
headers: {
|
|
769
|
+
...authHeaders,
|
|
770
|
+
"x-mockingbird-namespace": namespace,
|
|
771
|
+
accept: "application/json"
|
|
772
|
+
}
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
if (!response.ok) {
|
|
776
|
+
const text = await response.text().catch(() => "");
|
|
777
|
+
throw new Error(`GET ${path}: HTTP ${response.status} ${text.slice(0, 200)}`);
|
|
778
|
+
}
|
|
779
|
+
return response.json();
|
|
780
|
+
};
|
|
781
|
+
const identify = async (endpoint) => {
|
|
782
|
+
if (endpoint.service) return;
|
|
783
|
+
const health = await adminGet(endpoint, "default")("/health");
|
|
784
|
+
endpoint.service = asServiceKind(health.service);
|
|
785
|
+
if (!endpoint.service) throw new Error(`not a messaging emulator (${String(health.service)})`);
|
|
786
|
+
};
|
|
787
|
+
const bucket = (namespace) => {
|
|
788
|
+
let b = buckets.get(namespace);
|
|
789
|
+
if (!b) {
|
|
790
|
+
b = { notifications: /* @__PURE__ */ new Map(), inFlight: null, queued: null, version: 0 };
|
|
791
|
+
buckets.set(namespace, b);
|
|
792
|
+
}
|
|
793
|
+
return b;
|
|
794
|
+
};
|
|
795
|
+
const readAll = async (namespace) => {
|
|
796
|
+
await rediscover();
|
|
797
|
+
const results = await Promise.all(
|
|
798
|
+
endpoints.map(async (endpoint) => {
|
|
799
|
+
const key = endpoint.name;
|
|
800
|
+
const status = statuses.get(key) ?? {
|
|
801
|
+
name: endpoint.name,
|
|
802
|
+
service: endpoint.service,
|
|
803
|
+
url: endpoint.adminUrl,
|
|
804
|
+
ok: false,
|
|
805
|
+
error: null,
|
|
806
|
+
count: 0,
|
|
807
|
+
lastSyncAt: null
|
|
808
|
+
};
|
|
809
|
+
statuses.set(key, status);
|
|
810
|
+
try {
|
|
811
|
+
await identify(endpoint);
|
|
812
|
+
const service = endpoint.service;
|
|
813
|
+
if (!service) return { ok: false };
|
|
814
|
+
const items = await READERS[service]({
|
|
815
|
+
source: endpoint.name,
|
|
816
|
+
namespace,
|
|
817
|
+
read: adminGet(endpoint, namespace)
|
|
818
|
+
});
|
|
819
|
+
Object.assign(status, {
|
|
820
|
+
service,
|
|
821
|
+
url: endpoint.adminUrl,
|
|
822
|
+
ok: true,
|
|
823
|
+
error: null,
|
|
824
|
+
lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
825
|
+
});
|
|
826
|
+
return { ok: true, items };
|
|
827
|
+
} catch (error) {
|
|
828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
829
|
+
if (message.startsWith("not a messaging emulator")) {
|
|
830
|
+
statuses.delete(key);
|
|
831
|
+
rejected.add(`${endpoint.name}|${endpoint.adminUrl}`);
|
|
832
|
+
endpoints = endpoints.filter((e) => e !== endpoint);
|
|
833
|
+
return { ok: false };
|
|
834
|
+
}
|
|
835
|
+
Object.assign(status, { url: endpoint.adminUrl, ok: false, error: message });
|
|
836
|
+
discoveredAt = 0;
|
|
837
|
+
return { ok: false, source: endpoint.name };
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
const b = bucket(namespace);
|
|
842
|
+
const failed = new Set(results.flatMap((r) => "source" in r && r.source ? [r.source] : []));
|
|
843
|
+
const fresh = dedupe(results.flatMap((r) => r.ok ? r.items : []));
|
|
844
|
+
const next = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const n of b.notifications.values()) if (failed.has(n.source)) next.set(n.id, n);
|
|
846
|
+
for (const n of fresh) next.set(n.id, n);
|
|
847
|
+
const added = [...next.values()].filter((n) => !b.notifications.has(n.id)).sort(byTime);
|
|
848
|
+
const removed = [...b.notifications.keys()].some((id) => !next.has(id));
|
|
849
|
+
const changed = added.length > 0 || removed || [...next.values()].some((n) => b.notifications.get(n.id)?.status !== n.status);
|
|
850
|
+
b.notifications = next;
|
|
851
|
+
for (const status of statuses.values())
|
|
852
|
+
status.count = [...buckets.values()].reduce(
|
|
853
|
+
(sum, bk) => sum + [...bk.notifications.values()].filter((n) => n.source === status.name).length,
|
|
854
|
+
0
|
|
855
|
+
);
|
|
856
|
+
if (changed) {
|
|
857
|
+
b.version++;
|
|
858
|
+
for (const listener of listeners) listener({ namespace, added, version: b.version });
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
const sync = (namespace = "default") => {
|
|
862
|
+
watched.add(namespace);
|
|
863
|
+
const b = bucket(namespace);
|
|
864
|
+
if (!b.inFlight) {
|
|
865
|
+
b.inFlight = readAll(namespace).finally(() => {
|
|
866
|
+
b.inFlight = null;
|
|
867
|
+
});
|
|
868
|
+
return b.inFlight;
|
|
869
|
+
}
|
|
870
|
+
b.queued ??= b.inFlight.then(() => {
|
|
871
|
+
b.queued = null;
|
|
872
|
+
return sync(namespace);
|
|
873
|
+
});
|
|
874
|
+
return b.queued;
|
|
875
|
+
};
|
|
876
|
+
const list2 = async (filter = {}) => {
|
|
877
|
+
const namespace = filter.namespace ?? "default";
|
|
878
|
+
await sync(namespace);
|
|
879
|
+
return [...bucket(namespace).notifications.values()].filter((n) => matches(n, { ...filter, namespace })).sort(byTime);
|
|
880
|
+
};
|
|
881
|
+
const waitFor = async (filter, wait = {}) => {
|
|
882
|
+
const timeoutMs = wait.timeoutMs ?? 1e4;
|
|
883
|
+
const count = wait.count ?? 1;
|
|
884
|
+
const deadline = Date.now() + timeoutMs;
|
|
885
|
+
for (; ; ) {
|
|
886
|
+
const found = await list2(filter);
|
|
887
|
+
if (found.length >= count) return found;
|
|
888
|
+
if (Date.now() >= deadline) {
|
|
889
|
+
const error = new Error(
|
|
890
|
+
`timed out after ${timeoutMs}ms waiting for ${count} notification(s) matching ${JSON.stringify(filter)}; found ${found.length}`
|
|
891
|
+
);
|
|
892
|
+
Object.assign(error, { found });
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
await new Promise(
|
|
896
|
+
(resolve) => setTimeout(resolve, Math.min(100, Math.max(0, deadline - Date.now())))
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const interval = options.pollIntervalMs ?? 750;
|
|
901
|
+
const timer = interval > 0 ? setInterval(() => {
|
|
902
|
+
for (const namespace of watched) void sync(namespace).catch(() => {
|
|
903
|
+
});
|
|
904
|
+
}, interval) : null;
|
|
905
|
+
timer?.unref?.();
|
|
906
|
+
const events = (namespace, signal) => {
|
|
907
|
+
watched.add(namespace);
|
|
908
|
+
const encoder = new TextEncoder();
|
|
909
|
+
let cleanup = () => {
|
|
910
|
+
};
|
|
911
|
+
return new ReadableStream({
|
|
912
|
+
start(controller) {
|
|
913
|
+
const send = (event, data) => {
|
|
914
|
+
try {
|
|
915
|
+
controller.enqueue(encoder.encode(`event: ${event}
|
|
916
|
+
data: ${JSON.stringify(data)}
|
|
917
|
+
|
|
918
|
+
`));
|
|
919
|
+
} catch {
|
|
920
|
+
cleanup();
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
const listener = (event) => {
|
|
924
|
+
if (event.namespace === namespace) send("change", event);
|
|
925
|
+
};
|
|
926
|
+
listeners.add(listener);
|
|
927
|
+
const heartbeat = setInterval(() => send("sources", [...statuses.values()]), 2e3);
|
|
928
|
+
cleanup = () => {
|
|
929
|
+
listeners.delete(listener);
|
|
930
|
+
clearInterval(heartbeat);
|
|
931
|
+
};
|
|
932
|
+
signal.addEventListener("abort", () => {
|
|
933
|
+
cleanup();
|
|
934
|
+
try {
|
|
935
|
+
controller.close();
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
send("hello", { namespace, version: bucket(namespace).version });
|
|
940
|
+
void sync(namespace).catch(() => {
|
|
941
|
+
});
|
|
942
|
+
},
|
|
943
|
+
cancel() {
|
|
944
|
+
cleanup();
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
const namespacesList = () => [.../* @__PURE__ */ new Set([...watched, ...buckets.keys()])].sort();
|
|
949
|
+
const handle = async (request) => {
|
|
950
|
+
const url = new URL(request.url);
|
|
951
|
+
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
952
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
953
|
+
return errorBody(405, "the notification harness is read-only");
|
|
954
|
+
if (path === "/" || path === "/index.html")
|
|
955
|
+
return new Response(renderPage({ title: options.title ?? "Notifications" }), {
|
|
956
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }
|
|
957
|
+
});
|
|
958
|
+
if (path === "/ui.js")
|
|
959
|
+
return new Response(BROWSER_JS, {
|
|
960
|
+
headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" }
|
|
961
|
+
});
|
|
962
|
+
if (path === "/api/health") return json(200, { status: "ok", sources: [...statuses.values()] });
|
|
963
|
+
if (path === "/api/sources") {
|
|
964
|
+
await rediscover();
|
|
965
|
+
return json(200, {
|
|
966
|
+
discoveryError,
|
|
967
|
+
sources: endpoints.map(
|
|
968
|
+
(e) => statuses.get(e.name) ?? {
|
|
969
|
+
name: e.name,
|
|
970
|
+
service: e.service,
|
|
971
|
+
url: e.adminUrl,
|
|
972
|
+
ok: false,
|
|
973
|
+
error: null,
|
|
974
|
+
count: 0,
|
|
975
|
+
lastSyncAt: null
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
services: SERVICE_KINDS
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
if (path === "/api/namespaces") {
|
|
982
|
+
await rediscover();
|
|
983
|
+
return json(200, { namespaces: namespacesList() });
|
|
984
|
+
}
|
|
985
|
+
let filter;
|
|
986
|
+
try {
|
|
987
|
+
filter = filterFromSearch(url.searchParams);
|
|
988
|
+
} catch (error) {
|
|
989
|
+
return errorBody(400, error instanceof Error ? error.message : String(error));
|
|
990
|
+
}
|
|
991
|
+
const namespace = filter.namespace ?? "default";
|
|
992
|
+
if (path === "/api/events")
|
|
993
|
+
return new Response(events(namespace, request.signal), {
|
|
994
|
+
headers: {
|
|
995
|
+
"content-type": "text/event-stream",
|
|
996
|
+
"cache-control": "no-store",
|
|
997
|
+
connection: "keep-alive"
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
if (path === "/api/notifications") {
|
|
1001
|
+
const found = await list2(filter);
|
|
1002
|
+
if (url.searchParams.get("order") === "newest") found.reverse();
|
|
1003
|
+
return json(200, { namespace, version: bucket(namespace).version, notifications: found });
|
|
1004
|
+
}
|
|
1005
|
+
if (path === "/api/notifications/latest") {
|
|
1006
|
+
const found = await list2(filter);
|
|
1007
|
+
const latest = found.at(-1);
|
|
1008
|
+
return latest ? json(200, latest) : errorBody(404, "no notification matches");
|
|
1009
|
+
}
|
|
1010
|
+
if (path === "/api/wait") {
|
|
1011
|
+
const timeoutMs = Number(url.searchParams.get("timeoutMs") ?? 1e4);
|
|
1012
|
+
const count = Number(url.searchParams.get("count") ?? 1);
|
|
1013
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 12e4)
|
|
1014
|
+
return errorBody(400, "timeoutMs: expected 0\u2013120000");
|
|
1015
|
+
if (!Number.isInteger(count) || count < 1)
|
|
1016
|
+
return errorBody(400, "count: expected a positive integer");
|
|
1017
|
+
try {
|
|
1018
|
+
return json(200, { namespace, notifications: await waitFor(filter, { timeoutMs, count }) });
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
return json(408, {
|
|
1021
|
+
error: {
|
|
1022
|
+
type: "mockingbird_notifications",
|
|
1023
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1024
|
+
},
|
|
1025
|
+
notifications: error.found ?? []
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const one = /^\/api\/notifications\/(.+)$/.exec(path);
|
|
1030
|
+
if (one) {
|
|
1031
|
+
const id = decodeURIComponent(one[1] ?? "");
|
|
1032
|
+
const ns = id.split(":")[1] ?? namespace;
|
|
1033
|
+
await sync(ns);
|
|
1034
|
+
const found = bucket(ns).notifications.get(id);
|
|
1035
|
+
return found ? json(200, found) : errorBody(404, `no notification ${id}`);
|
|
1036
|
+
}
|
|
1037
|
+
return errorBody(404, `no route ${path}`);
|
|
1038
|
+
};
|
|
1039
|
+
return {
|
|
1040
|
+
fetch: async (request) => {
|
|
1041
|
+
try {
|
|
1042
|
+
return await handle(request);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
return errorBody(500, error instanceof Error ? error.message : String(error));
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
sync,
|
|
1048
|
+
list: list2,
|
|
1049
|
+
waitFor,
|
|
1050
|
+
sources: () => [...statuses.values()],
|
|
1051
|
+
close: () => {
|
|
1052
|
+
if (timer) clearInterval(timer);
|
|
1053
|
+
listeners.clear();
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
export {
|
|
1059
|
+
SERVICE_KINDS,
|
|
1060
|
+
SERVICE_LABELS,
|
|
1061
|
+
matches,
|
|
1062
|
+
filterToSearch,
|
|
1063
|
+
parseMime,
|
|
1064
|
+
createNotificationHarness
|
|
1065
|
+
};
|
|
1066
|
+
//# sourceMappingURL=chunk-GRGR3QZM.js.map
|