viveworker 0.3.0 → 0.4.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/web/app.js CHANGED
@@ -542,6 +542,7 @@ function normalizeProviderClient(value) {
542
542
  const normalized = String(value || "").toLowerCase();
543
543
  if (normalized === "claude") return "claude";
544
544
  if (normalized === "moltbook") return "moltbook";
545
+ if (normalized === "a2a") return "a2a";
545
546
  return "codex";
546
547
  }
547
548
 
@@ -549,6 +550,7 @@ function providerDisplayName(provider) {
549
550
  const p = normalizeProviderClient(provider);
550
551
  if (p === "claude") return L("common.claude");
551
552
  if (p === "moltbook") return "Moltbook";
553
+ if (p === "a2a") return "A2A";
552
554
  return L("common.codex");
553
555
  }
554
556
 
@@ -1703,6 +1705,9 @@ function providerBadgeMeta(provider) {
1703
1705
  if (normalized === "moltbook") {
1704
1706
  return { id: "moltbook", label: "Moltbook", glyph: "M" };
1705
1707
  }
1708
+ if (normalized === "a2a") {
1709
+ return { id: "a2a", label: "A2A", glyph: "A" };
1710
+ }
1706
1711
  return { id: "codex", label: L("common.codex"), glyph: "X" };
1707
1712
  }
1708
1713
 
@@ -1720,6 +1725,9 @@ function providerFilterOptions() {
1720
1725
  if (state.session?.moltbookEnabled === true) {
1721
1726
  options.push({ id: "moltbook", label: "Moltbook" });
1722
1727
  }
1728
+ if (state.session?.a2aEnabled === true) {
1729
+ options.push({ id: "a2a", label: "A2A" });
1730
+ }
1723
1731
  return options;
1724
1732
  }
1725
1733
 
@@ -3031,7 +3039,7 @@ function renderSettingsNotificationsPage(context) {
3031
3039
  const { push, permission, secureContext, standalone, supportsPushValue, serverEnabled } = context;
3032
3040
  const statusRows = [
3033
3041
  renderSettingsInfoRow(L("settings.row.status"), L(context.setupState.notifications.labelKey)),
3034
- renderSettingsInfoRow(L("settings.row.notificationPermission"), permission),
3042
+ renderSettingsInfoRow(L("settings.row.notificationPermission"), L(`settings.permission.${permission}`) || permission),
3035
3043
  renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
3036
3044
  push.lastSuccessfulDeliveryAtMs
3037
3045
  ? renderSettingsInfoRow(
@@ -3142,7 +3150,7 @@ function renderSettingsAdvancedPage(context) {
3142
3150
  renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
3143
3151
  renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.supported") : L("common.notSupported")),
3144
3152
  renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone ? L("common.supported") : L("common.notSupported")),
3145
- renderSettingsInfoRow(L("settings.row.notificationPermission"), context.permission),
3153
+ renderSettingsInfoRow(L("settings.row.notificationPermission"), L(`settings.permission.${context.permission}`) || context.permission),
3146
3154
  renderSettingsInfoRow(L("settings.row.browserSupport"), context.supportsPushValue ? L("common.supported") : L("common.notSupported")),
3147
3155
  renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), context.push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
3148
3156
  context.push.lastSuccessfulDeliveryAtMs
@@ -3248,13 +3256,18 @@ function renderSettingsMoltbookPage(context) {
3248
3256
  ${renderSettingsGroup("", [
3249
3257
  renderSettingsInfoRow(L("settings.row.moltbookQuota"), `${scout.sentToday} / ${scout.maxDaily}`),
3250
3258
  renderSettingsInfoRow(L("settings.row.moltbookComposed"), `${scout.composedToday || 0} / 3`),
3251
- renderSettingsInfoRow(L("settings.row.moltbookSlots"), (scout.composeSlotsAttempted || []).join(", ") || "—"),
3252
- renderSettingsInfoRow(L("settings.row.moltbookDate"), scout.day),
3253
3259
  renderSettingsInfoRow(L("settings.row.moltbookSeenPosts"), String(scout.seenPostCount)),
3254
3260
  ])}
3255
3261
  ${batchRows.length ? renderSettingsGroup(L("settings.moltbook.batchTitle"), batchRows) : ""}
3256
3262
  ${Array.isArray(scout.recentComposeTitles) && scout.recentComposeTitles.length
3257
- ? renderSettingsGroup(L("settings.row.moltbookRecentTitles"), scout.recentComposeTitles.map((t) => renderSettingsInfoRow("", t)))
3263
+ ? renderSettingsGroup(L("settings.row.moltbookRecentTitles"), scout.recentComposeTitles.map((t) => {
3264
+ const title = typeof t === "string" ? t : (t.title || "");
3265
+ const postId = typeof t === "object" ? t.postId : "";
3266
+ const display = postId
3267
+ ? `<a href="https://www.moltbook.com/post/${escapeHtml(postId)}" target="_blank" rel="noopener">${escapeHtml(title)}</a>`
3268
+ : escapeHtml(title);
3269
+ return renderSettingsInfoRow("", display, { rawValue: true, rowClassName: "settings-info-row--stacked" });
3270
+ }))
3258
3271
  : ""}
3259
3272
  </div>
3260
3273
  `;
@@ -3263,10 +3276,11 @@ function renderSettingsMoltbookPage(context) {
3263
3276
  function renderSettingsInfoRow(label, value, options = {}) {
3264
3277
  const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
3265
3278
  const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
3279
+ const displayValue = options.rawValue ? value : escapeHtml(value);
3266
3280
  return `
3267
3281
  <div class="${rowClassName}">
3268
3282
  <span class="settings-info-row__label">${escapeHtml(label)}</span>
3269
- <span class="${valueClassName}">${escapeHtml(value)}</span>
3283
+ <span class="${valueClassName}">${displayValue}</span>
3270
3284
  </div>
3271
3285
  `;
3272
3286
  }
@@ -3377,8 +3391,9 @@ function renderStandardDetailDesktop(detail) {
3377
3391
  ${renderInterruptedDetailNotice(detail)}
3378
3392
  ${renderMoltbookReplyComposer(detail)}
3379
3393
  ${renderMoltbookDraftComposer(detail)}
3394
+ ${renderA2ATaskDetail(detail)}
3380
3395
  ${
3381
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply"
3396
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3382
3397
  ? ""
3383
3398
  : plainIntro
3384
3399
  ? plainIntro
@@ -3413,8 +3428,9 @@ function renderStandardDetailMobile(detail) {
3413
3428
  ${renderInterruptedDetailNotice(detail, { mobile: true })}
3414
3429
  ${renderMoltbookReplyComposer(detail, { mobile: true })}
3415
3430
  ${renderMoltbookDraftComposer(detail, { mobile: true })}
3431
+ ${renderA2ATaskDetail(detail, { mobile: true })}
3416
3432
  ${
3417
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply"
3433
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3418
3434
  ? ""
3419
3435
  : plainIntro
3420
3436
  ? plainIntro
@@ -3925,6 +3941,15 @@ function renderMoltbookDraftComposer(detail, options = {}) {
3925
3941
  : "";
3926
3942
  const eyebrowLabel = isOriginalPost ? L("moltbook.draft.eyebrowPost") : L("moltbook.draft.eyebrowReply");
3927
3943
  const approveLabel = isOriginalPost ? L("moltbook.draft.approvePost") : L("moltbook.draft.approveReply");
3944
+ const greetingBlock = (() => {
3945
+ if (!isOriginalPost) return "";
3946
+ const slot = detail.slot || "";
3947
+ const icons = { morning: "\u2600\ufe0f", noon: "\u26c5", evening: "\ud83c\udf19" };
3948
+ const keys = { morning: "moltbook.draft.greetMorning", noon: "moltbook.draft.greetNoon", evening: "moltbook.draft.greetEvening" };
3949
+ const icon = icons[slot] || "\u270d\ufe0f";
3950
+ const msg = keys[slot] ? L(keys[slot]) : L("moltbook.draft.greetFallback");
3951
+ return `<div class="compose-greeting"><span class="compose-greeting__icon">${icon}</span><span class="compose-greeting__text">${escapeHtml(msg)}</span></div>`;
3952
+ })();
3928
3953
  const buttons = enabled
3929
3954
  ? `
3930
3955
  <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
@@ -3940,6 +3965,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
3940
3965
  <div class="reply-composer__copy">
3941
3966
  <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(eyebrowLabel)}</span>
3942
3967
  ${submoltBadge}
3968
+ ${greetingBlock}
3943
3969
  ${postLink}
3944
3970
  ${postAuthorLine}
3945
3971
  ${titleInput}
@@ -3954,6 +3980,58 @@ function renderMoltbookDraftComposer(detail, options = {}) {
3954
3980
  `;
3955
3981
  }
3956
3982
 
3983
+ function renderA2ATaskDetail(detail, options = {}) {
3984
+ if (detail.kind !== "a2a_task") return "";
3985
+ const enabled = detail.a2aTaskEnabled !== false && detail.readOnly !== true;
3986
+ const instruction = detail.instruction || "";
3987
+ const callerIp = detail.callerInfo?.ip || "";
3988
+ const callerAgent = detail.callerInfo?.userAgent || "";
3989
+ const callerLine = callerIp
3990
+ ? `<p class="reply-composer__author-meta muted">${escapeHtml(L("a2a.task.from"))}: ${escapeHtml(callerIp)}${callerAgent ? ` (${escapeHtml(callerAgent.slice(0, 60))})` : ""}</p>`
3991
+ : "";
3992
+
3993
+ const statusKey = {
3994
+ submitted: "a2a.task.statusSubmitted",
3995
+ working: "a2a.task.statusWorking",
3996
+ completed: "a2a.task.statusCompleted",
3997
+ failed: "a2a.task.statusFailed",
3998
+ canceled: "a2a.task.statusCanceled",
3999
+ rejected: "a2a.task.statusRejected",
4000
+ }[detail.taskStatus] || "a2a.task.statusSubmitted";
4001
+
4002
+ const statusBadge = !enabled
4003
+ ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
4004
+ : "";
4005
+
4006
+ const buttons = enabled
4007
+ ? `
4008
+ <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
4009
+ <button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(L("a2a.task.approve"))}</button>
4010
+ <button type="submit" data-action="deny" class="danger danger--wide">${escapeHtml(L("a2a.task.deny"))}</button>
4011
+ </div>
4012
+ `
4013
+ : `<p class="muted reply-composer__description">${escapeHtml(L("a2a.task.resolved"))}</p>`;
4014
+ const buttonsWrapped = enabled && options.mobile ? `<div class="detail-action-bar">${buttons}</div>` : buttons;
4015
+
4016
+ return `
4017
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
4018
+ <form class="reply-composer" data-a2a-task-form data-token="${escapeHtml(detail.token || "")}">
4019
+ <div class="reply-composer__copy">
4020
+ <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.eyebrow"))}</span>
4021
+ ${statusBadge}
4022
+ ${callerLine}
4023
+ <p class="muted reply-composer__description">${enabled ? escapeHtml(L("a2a.task.editHint")) : ""}</p>
4024
+ </div>
4025
+ <div class="reply-composer__instruction">
4026
+ <label class="field-label">${escapeHtml(L("a2a.task.instruction"))}</label>
4027
+ <textarea name="instruction" class="reply-composer__textarea" rows="6" ${enabled ? "" : "readonly"}>${escapeHtml(instruction)}</textarea>
4028
+ </div>
4029
+ ${buttonsWrapped}
4030
+ </form>
4031
+ </section>
4032
+ `;
4033
+ }
4034
+
3957
4035
  function renderCompletionReplyComposer(detail, options = {}) {
3958
4036
  if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
3959
4037
  return "";
@@ -5081,6 +5159,72 @@ function bindShellInteractions() {
5081
5159
  });
5082
5160
  }
5083
5161
 
5162
+ const a2aTaskForm = document.querySelector("[data-a2a-task-form]");
5163
+ if (a2aTaskForm) {
5164
+ const token = a2aTaskForm.dataset.token || "";
5165
+ let submittedAction = "approve";
5166
+ a2aTaskForm.querySelectorAll("button[type='submit']").forEach((btn) => {
5167
+ btn.addEventListener("click", () => {
5168
+ submittedAction = btn.dataset.action || "approve";
5169
+ });
5170
+ });
5171
+ a2aTaskForm.addEventListener("submit", async (event) => {
5172
+ event.preventDefault();
5173
+ if (a2aTaskForm.dataset.submitting === "1") return;
5174
+ a2aTaskForm.dataset.submitting = "1";
5175
+ const buttons = a2aTaskForm.querySelectorAll("button[type='submit']");
5176
+ const textarea = a2aTaskForm.querySelector("textarea");
5177
+ const labelCache = new Map();
5178
+ buttons.forEach((btn) => {
5179
+ labelCache.set(btn, btn.innerHTML);
5180
+ btn.disabled = true;
5181
+ if (btn.dataset.action === submittedAction) {
5182
+ btn.innerHTML = submittedAction === "approve" ? "Executing…" : "Denying…";
5183
+ btn.classList.add("is-loading");
5184
+ } else {
5185
+ btn.classList.add("is-dimmed");
5186
+ }
5187
+ });
5188
+ if (textarea) textarea.readOnly = true;
5189
+ const instruction = normalizeClientText(new FormData(a2aTaskForm).get("instruction"));
5190
+ try {
5191
+ const res = await fetch(`/api/items/a2a-task/${encodeURIComponent(token)}/decision`, {
5192
+ method: "POST",
5193
+ headers: { "content-type": "application/json" },
5194
+ credentials: "same-origin",
5195
+ body: JSON.stringify({ action: submittedAction, instruction }),
5196
+ });
5197
+ if (!res.ok) {
5198
+ const errBody = await res.json().catch(() => ({}));
5199
+ alert(`A2A task ${submittedAction} failed: ${errBody.error || res.status}`);
5200
+ buttons.forEach((btn) => {
5201
+ btn.disabled = false;
5202
+ btn.classList.remove("is-loading", "is-dimmed");
5203
+ btn.innerHTML = labelCache.get(btn);
5204
+ });
5205
+ if (textarea) textarea.readOnly = false;
5206
+ a2aTaskForm.dataset.submitting = "";
5207
+ return;
5208
+ }
5209
+ if (state.currentDetail?.kind === "a2a_task") {
5210
+ state.currentDetail.a2aTaskEnabled = false;
5211
+ state.currentDetail.readOnly = true;
5212
+ }
5213
+ await refreshAuthenticatedState();
5214
+ await renderShell();
5215
+ } catch (error) {
5216
+ alert(`A2A task error: ${error.message}`);
5217
+ buttons.forEach((btn) => {
5218
+ btn.disabled = false;
5219
+ btn.classList.remove("is-loading", "is-dimmed");
5220
+ btn.innerHTML = labelCache.get(btn);
5221
+ });
5222
+ if (textarea) textarea.readOnly = false;
5223
+ a2aTaskForm.dataset.submitting = "";
5224
+ }
5225
+ });
5226
+ }
5227
+
5084
5228
  const replyForm = document.querySelector("[data-completion-reply-form]");
5085
5229
  if (replyForm) {
5086
5230
  const token = replyForm.dataset.token || "";
package/web/i18n.js CHANGED
@@ -336,6 +336,9 @@ const translations = {
336
336
  "settings.row.secureContext": "Secure context",
337
337
  "settings.row.homeScreenApp": "Home Screen app",
338
338
  "settings.row.notificationPermission": "Notification permission",
339
+ "settings.permission.granted": "granted",
340
+ "settings.permission.denied": "denied",
341
+ "settings.permission.default": "default",
339
342
  "settings.row.browserSupport": "Browser support",
340
343
  "settings.row.pushStatus": "Push notifications",
341
344
  "settings.row.currentDeviceSubscribed": "This device subscribed",
@@ -348,16 +351,14 @@ const translations = {
348
351
  "settings.moltbook.subtitle": "Auto-scout posting quota",
349
352
  "settings.moltbook.copy": "View your daily auto-scout posting status and quota.",
350
353
  "settings.moltbook.unavailable": "Moltbook is not configured.",
351
- "settings.row.moltbookQuota": "Posted today",
352
- "settings.row.moltbookDate": "Date",
354
+ "settings.row.moltbookQuota": "Replies today",
353
355
  "settings.row.moltbookSeenPosts": "Seen posts",
354
356
  "settings.moltbook.batchTitle": "Current batch",
355
357
  "settings.row.moltbookBatchCandidates": "Candidates",
356
358
  "settings.row.moltbookBatchTopScore": "Top score",
357
359
  "settings.row.moltbookBatchRemaining": "Time remaining",
358
360
  "settings.row.moltbookComposed": "Original posts today",
359
- "settings.row.moltbookSlots": "Slots attempted",
360
- "settings.row.moltbookRecentTitles": "Recent post titles",
361
+ "settings.row.moltbookRecentTitles": "Recent original posts",
361
362
  "moltbook.draft.eyebrowPost": "Moltbook new post",
362
363
  "moltbook.draft.eyebrowReply": "Moltbook draft",
363
364
  "moltbook.draft.intent": "Intent",
@@ -366,6 +367,23 @@ const translations = {
366
367
  "moltbook.draft.approveReply": "Approve & post",
367
368
  "moltbook.draft.editHint": "Edit if needed, then approve to publish or deny to skip.",
368
369
  "moltbook.draft.resolved": "This draft has already been resolved.",
370
+ "moltbook.draft.greetMorning": "Good morning! Share yesterday's highlights?",
371
+ "moltbook.draft.greetNoon": "Nice progress! Ready to share your morning work?",
372
+ "moltbook.draft.greetEvening": "Great work today! Let's share what you built.",
373
+ "moltbook.draft.greetFallback": "New post ready for review",
374
+ "a2a.task.eyebrow": "A2A Task Request",
375
+ "a2a.task.from": "From",
376
+ "a2a.task.instruction": "Task",
377
+ "a2a.task.approve": "Approve & Execute",
378
+ "a2a.task.deny": "Deny",
379
+ "a2a.task.editHint": "Review the task request, then approve to execute or deny to reject.",
380
+ "a2a.task.resolved": "This task has already been resolved.",
381
+ "a2a.task.statusSubmitted": "Awaiting your decision",
382
+ "a2a.task.statusWorking": "Executing...",
383
+ "a2a.task.statusCompleted": "Completed",
384
+ "a2a.task.statusFailed": "Failed",
385
+ "a2a.task.statusCanceled": "Canceled",
386
+ "a2a.task.statusRejected": "Rejected",
369
387
  "notice.notificationsEnabled": "Notifications are enabled on this device.",
370
388
  "notice.notificationsDisabled": "Notifications are disabled on this device.",
371
389
  "notice.testNotificationSent": "Test notification sent.",
@@ -905,6 +923,9 @@ const translations = {
905
923
  "settings.row.secureContext": "Secure context",
906
924
  "settings.row.homeScreenApp": "ホーム画面アプリ",
907
925
  "settings.row.notificationPermission": "通知権限",
926
+ "settings.permission.granted": "付与済",
927
+ "settings.permission.denied": "拒否",
928
+ "settings.permission.default": "未設定",
908
929
  "settings.row.browserSupport": "ブラウザ対応",
909
930
  "settings.row.pushStatus": "Push 通知",
910
931
  "settings.row.currentDeviceSubscribed": "この端末の購読状態",
@@ -917,16 +938,14 @@ const translations = {
917
938
  "settings.moltbook.subtitle": "自動スカウトの投稿枠",
918
939
  "settings.moltbook.copy": "本日の自動スカウト投稿状況と上限を確認できます。",
919
940
  "settings.moltbook.unavailable": "Moltbook が設定されていません。",
920
- "settings.row.moltbookQuota": "本日の投稿数",
921
- "settings.row.moltbookDate": "日付",
922
- "settings.row.moltbookSeenPosts": "確認済み投稿数",
941
+ "settings.row.moltbookQuota": "本日の返信数",
942
+ "settings.row.moltbookSeenPosts": "巡回済み投稿数",
923
943
  "settings.moltbook.batchTitle": "現在のバッチ",
924
944
  "settings.row.moltbookBatchCandidates": "候補数",
925
945
  "settings.row.moltbookBatchTopScore": "最高スコア",
926
946
  "settings.row.moltbookBatchRemaining": "残り時間",
927
947
  "settings.row.moltbookComposed": "本日の新規投稿数",
928
- "settings.row.moltbookSlots": "提案済みスロット",
929
- "settings.row.moltbookRecentTitles": "最近の投稿タイトル",
948
+ "settings.row.moltbookRecentTitles": "最近の新規投稿タイトル",
930
949
  "moltbook.draft.eyebrowPost": "Moltbook 新規投稿",
931
950
  "moltbook.draft.eyebrowReply": "Moltbook ドラフト",
932
951
  "moltbook.draft.intent": "意図",
@@ -935,6 +954,23 @@ const translations = {
935
954
  "moltbook.draft.approveReply": "承認して返信",
936
955
  "moltbook.draft.editHint": "必要に応じて編集し、承認して投稿するか、拒否してスキップします。",
937
956
  "moltbook.draft.resolved": "このドラフトは既に処理済みです。",
957
+ "moltbook.draft.greetMorning": "おはようございます!昨日の成果をシェアしませんか?",
958
+ "moltbook.draft.greetNoon": "午前中お疲れ様でした!進捗をシェアしませんか?",
959
+ "moltbook.draft.greetEvening": "今日もお疲れ様でした!成果をシェアしませんか?",
960
+ "moltbook.draft.greetFallback": "投稿ドラフトができました",
961
+ "a2a.task.eyebrow": "A2A タスクリクエスト",
962
+ "a2a.task.from": "送信元",
963
+ "a2a.task.instruction": "タスク内容",
964
+ "a2a.task.approve": "承認して実行",
965
+ "a2a.task.deny": "拒否",
966
+ "a2a.task.editHint": "タスクリクエストを確認し、承認して実行するか拒否してください。",
967
+ "a2a.task.resolved": "このタスクは既に処理済みです。",
968
+ "a2a.task.statusSubmitted": "承認待ち",
969
+ "a2a.task.statusWorking": "実行中...",
970
+ "a2a.task.statusCompleted": "完了",
971
+ "a2a.task.statusFailed": "失敗",
972
+ "a2a.task.statusCanceled": "キャンセル",
973
+ "a2a.task.statusRejected": "拒否済み",
938
974
  "notice.notificationsEnabled": "この端末で通知を有効にしました。",
939
975
  "notice.notificationsDisabled": "この端末の通知を無効にしました。",
940
976
  "notice.testNotificationSent": "テスト通知を送信しました。",
package/web/sw.js CHANGED
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "viveworker-v20";
1
+ const CACHE_NAME = "viveworker-v25";
2
2
  const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
3
3
  const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
4
4
  const APP_ASSETS = ["/app.css", "/app.js", "/i18n.js"];