aimeat 1.23.2 → 1.23.3
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/dist/locales/en.json +536 -21
- package/dist/locales/fi.json +548 -33
- package/dist/public/components/ContactCard.js +28 -0
- package/dist/public/css/views/admin.css +12 -0
- package/dist/public/css/views/agents-detail.css +62 -10
- package/dist/public/css/views/landing.css +87 -0
- package/dist/public/css/views/offers.css +96 -0
- package/dist/public/css/views/portfolio.css +18 -0
- package/dist/public/css/views/profile.css +153 -8
- package/dist/public/css/views/start.css +96 -0
- package/dist/public/img/business-hero.png +0 -0
- package/dist/public/img/toimitus.png +0 -0
- package/dist/public/js/components/tag-cloud.js +46 -31
- package/dist/public/js/components/tag-editor.js +2 -2
- package/dist/public/js/recents.js +40 -0
- package/dist/public/js/services/admin.js +6 -0
- package/dist/public/js/services/agents.js +34 -0
- package/dist/public/js/services/offers.js +152 -0
- package/dist/public/js/services/packages.js +1 -0
- package/dist/public/spa.html +32 -2
- package/dist/public/views/admin/subdomains-tab.js +182 -0
- package/dist/public/views/admin.js +2 -0
- package/dist/public/views/business.js +119 -0
- package/dist/public/views/how-it-works.js +159 -0
- package/dist/public/views/landing.js +235 -0
- package/dist/public/views/portfolio.js +120 -23
- package/dist/public/views/pricing.js +75 -0
- package/dist/public/views/profile/access-tab.js +124 -51
- package/dist/public/views/profile/agents/agent-card.js +68 -37
- package/dist/public/views/profile/agents/shared-board.js +56 -22
- package/dist/public/views/profile/agents/tab-activity.js +24 -12
- package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
- package/dist/public/views/profile/agents/tab-data-access.js +9 -13
- package/dist/public/views/profile/agents/tab-integration.js +27 -1
- package/dist/public/views/profile/agents/tab-messages.js +6 -11
- package/dist/public/views/profile/agents/tab-quality.js +45 -5
- package/dist/public/views/profile/agents-tab.js +41 -6
- package/dist/public/views/profile/apps-tab.js +7 -1
- package/dist/public/views/profile/knowledge-tab.js +4 -1
- package/dist/public/views/profile/landing-page.js +427 -123
- package/dist/public/views/profile/memory-tab.js +409 -211
- package/dist/public/views/profile/nodes-tab.js +18 -3
- package/dist/public/views/profile/offers-tab.js +310 -0
- package/dist/public/views/profile/organisms-tab.js +289 -28
- package/dist/public/views/profile/packages-tab.js +152 -16
- package/dist/public/views/profile/portfolio-tab.js +98 -31
- package/dist/public/views/profile.js +2 -0
- package/dist/public/views/start-flows.js +281 -0
- package/dist/public/views/start.js +258 -0
- package/dist/src/generated/api-types.d.ts +856 -17
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +15 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
- package/dist/src/generated/prisma-postgres/index.js +15 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
- package/dist/src/generated/prisma-postgres/wasm.js +15 -3
- package/dist/src/middleware/subdomain.d.ts +28 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -0
- package/dist/src/middleware/subdomain.js +32 -0
- package/dist/src/middleware/subdomain.js.map +1 -0
- package/dist/src/models/offer-schemas.d.ts +229 -0
- package/dist/src/models/offer-schemas.d.ts.map +1 -0
- package/dist/src/models/offer-schemas.js +100 -0
- package/dist/src/models/offer-schemas.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +33 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +146 -1
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/apps-backup.d.ts +20 -0
- package/dist/src/routes/apps-backup.d.ts.map +1 -0
- package/dist/src/routes/apps-backup.js +165 -0
- package/dist/src/routes/apps-backup.js.map +1 -0
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +4 -0
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/instances.d.ts +2 -1
- package/dist/src/routes/instances.d.ts.map +1 -1
- package/dist/src/routes/instances.js +59 -1
- package/dist/src/routes/instances.js.map +1 -1
- package/dist/src/routes/packages.d.ts.map +1 -1
- package/dist/src/routes/packages.js +32 -0
- package/dist/src/routes/packages.js.map +1 -1
- package/dist/src/routes/permissions.d.ts +12 -0
- package/dist/src/routes/permissions.d.ts.map +1 -1
- package/dist/src/routes/permissions.js +35 -3
- package/dist/src/routes/permissions.js.map +1 -1
- package/dist/src/routes/portal.d.ts +3 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +12 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +15 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -0
- package/dist/src/routes/public-stats.js +90 -0
- package/dist/src/routes/public-stats.js.map +1 -0
- package/dist/src/routes/sse.d.ts +7 -2
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +35 -5
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +33 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -0
- package/dist/src/routes/subdomains.js +208 -0
- package/dist/src/routes/subdomains.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +12 -1
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +4 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/apps-backup-export.d.ts +22 -0
- package/dist/src/services/apps-backup-export.d.ts.map +1 -0
- package/dist/src/services/apps-backup-export.js +136 -0
- package/dist/src/services/apps-backup-export.js.map +1 -0
- package/dist/src/services/apps-backup-import.d.ts +120 -0
- package/dist/src/services/apps-backup-import.d.ts.map +1 -0
- package/dist/src/services/apps-backup-import.js +259 -0
- package/dist/src/services/apps-backup-import.js.map +1 -0
- package/dist/src/services/component-registrar.d.ts +17 -0
- package/dist/src/services/component-registrar.d.ts.map +1 -1
- package/dist/src/services/component-registrar.js +63 -5
- package/dist/src/services/component-registrar.js.map +1 -1
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +5 -2
- package/dist/src/services/handbooks/appdev.js.map +1 -1
- package/dist/src/storage/interface.d.ts +11 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +76 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +61 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +11 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/package.repository.d.ts +2 -0
- package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
- package/dist/static/app-catalog.html +1139 -28
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +11 -0
- package/prisma/schema.prisma +11 -0
|
@@ -1074,8 +1074,107 @@
|
|
|
1074
1074
|
|
|
1075
1075
|
/* ── Publish Modal ──────────────────────────────── */
|
|
1076
1076
|
|
|
1077
|
+
/* ── Backup menu + import modal ─────────────────── */
|
|
1078
|
+
|
|
1079
|
+
.backup-menu-wrap { position: relative; }
|
|
1080
|
+
|
|
1081
|
+
.backup-menu {
|
|
1082
|
+
position: absolute;
|
|
1083
|
+
top: calc(100% + 6px);
|
|
1084
|
+
right: 0;
|
|
1085
|
+
z-index: 600;
|
|
1086
|
+
min-width: 220px;
|
|
1087
|
+
background: var(--surface);
|
|
1088
|
+
border: 1px solid var(--border-subtle);
|
|
1089
|
+
border-radius: var(--radius-sm);
|
|
1090
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, .18);
|
|
1091
|
+
padding: 6px;
|
|
1092
|
+
display: flex;
|
|
1093
|
+
flex-direction: column;
|
|
1094
|
+
gap: 2px;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.backup-menu[hidden] { display: none; }
|
|
1098
|
+
|
|
1099
|
+
.backup-menu button {
|
|
1100
|
+
background: none;
|
|
1101
|
+
border: none;
|
|
1102
|
+
color: var(--text);
|
|
1103
|
+
text-align: left;
|
|
1104
|
+
padding: 8px 10px;
|
|
1105
|
+
border-radius: 6px;
|
|
1106
|
+
cursor: pointer;
|
|
1107
|
+
font-size: .85rem;
|
|
1108
|
+
white-space: nowrap;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.backup-menu button:hover { background: var(--hover); }
|
|
1112
|
+
|
|
1113
|
+
#backup-modal { max-height: 84vh; display: flex; flex-direction: column; }
|
|
1114
|
+
#backup-import-body { overflow-y: auto; flex: 1; min-height: 0; }
|
|
1115
|
+
|
|
1116
|
+
.backup-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
|
|
1117
|
+
.backup-table th {
|
|
1118
|
+
text-align: left; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
|
|
1119
|
+
color: var(--text-muted); padding: 6px 8px; border-bottom: 1px solid var(--border-subtle);
|
|
1120
|
+
}
|
|
1121
|
+
.backup-table td { padding: 6px 8px; border-bottom: 1px solid var(--border-subtle); vertical-align: top; }
|
|
1122
|
+
.backup-table .mono-cell { font-family: monospace; font-size: .78rem; color: var(--text-muted); }
|
|
1123
|
+
|
|
1124
|
+
.backup-versions-toggle {
|
|
1125
|
+
background: none; border: none; color: var(--accent); cursor: pointer;
|
|
1126
|
+
font-size: .78rem; padding: 0; text-decoration: underline;
|
|
1127
|
+
}
|
|
1128
|
+
.backup-version-list { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; }
|
|
1129
|
+
.backup-version-list label { font-size: .78rem; color: var(--text-muted); display: flex; gap: 6px; align-items: center; }
|
|
1130
|
+
|
|
1131
|
+
.backup-conflict-select {
|
|
1132
|
+
background: var(--bg-subtle); color: var(--text); border: 1px solid var(--border-subtle);
|
|
1133
|
+
border-radius: 6px; padding: 4px 6px; font-size: .78rem;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.backup-status-badge {
|
|
1137
|
+
display: inline-block; font-size: .62rem; font-weight: 600; text-transform: uppercase;
|
|
1138
|
+
padding: 2px 6px; border-radius: 4px;
|
|
1139
|
+
}
|
|
1140
|
+
.backup-status-new { background: rgba(52, 211, 153, .12); color: #34d399; border: 1px solid rgba(52, 211, 153, .3); }
|
|
1141
|
+
.backup-status-exists { background: rgba(245, 158, 11, .10); color: #d97706; border: 1px solid rgba(245, 158, 11, .25); }
|
|
1142
|
+
|
|
1143
|
+
.backup-toolbar-row {
|
|
1144
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
1145
|
+
gap: 8px; margin: 8px 0; flex-wrap: wrap;
|
|
1146
|
+
}
|
|
1147
|
+
.backup-summary-line { font-size: .82rem; color: var(--text-muted); }
|
|
1148
|
+
.backup-link-btn {
|
|
1149
|
+
background: none; border: none; color: var(--accent); cursor: pointer;
|
|
1150
|
+
font-size: .8rem; padding: 0 4px; text-decoration: underline;
|
|
1151
|
+
}
|
|
1152
|
+
.backup-result-list { font-size: .82rem; margin: 4px 0 10px; padding-left: 18px; }
|
|
1153
|
+
.backup-result-errors { color: #ef4444; }
|
|
1154
|
+
|
|
1155
|
+
.server-import-banner {
|
|
1156
|
+
margin: 0 24px 1rem;
|
|
1157
|
+
padding: 10px 14px;
|
|
1158
|
+
display: flex;
|
|
1159
|
+
justify-content: space-between;
|
|
1160
|
+
align-items: center;
|
|
1161
|
+
gap: 12px;
|
|
1162
|
+
flex-wrap: wrap;
|
|
1163
|
+
background: var(--surface-glass);
|
|
1164
|
+
border: 1px solid var(--border-subtle);
|
|
1165
|
+
border-radius: var(--radius-sm);
|
|
1166
|
+
font-size: .85rem;
|
|
1167
|
+
position: relative;
|
|
1168
|
+
z-index: 1;
|
|
1169
|
+
}
|
|
1170
|
+
.server-import-banner-actions { display: flex; gap: 8px; }
|
|
1171
|
+
|
|
1077
1172
|
#publish-overlay,
|
|
1078
|
-
#versions-overlay
|
|
1173
|
+
#versions-overlay,
|
|
1174
|
+
#subdomain-overlay,
|
|
1175
|
+
#backup-overlay,
|
|
1176
|
+
#server-import-overlay,
|
|
1177
|
+
#json-import-overlay {
|
|
1079
1178
|
position: fixed;
|
|
1080
1179
|
inset: 0;
|
|
1081
1180
|
z-index: 560;
|
|
@@ -1089,7 +1188,11 @@
|
|
|
1089
1188
|
}
|
|
1090
1189
|
|
|
1091
1190
|
#publish-overlay[hidden],
|
|
1092
|
-
#versions-overlay[hidden]
|
|
1191
|
+
#versions-overlay[hidden],
|
|
1192
|
+
#subdomain-overlay[hidden],
|
|
1193
|
+
#backup-overlay[hidden],
|
|
1194
|
+
#server-import-overlay[hidden],
|
|
1195
|
+
#json-import-overlay[hidden] {
|
|
1093
1196
|
display: none;
|
|
1094
1197
|
}
|
|
1095
1198
|
|
|
@@ -1272,6 +1375,43 @@
|
|
|
1272
1375
|
border: 1px solid rgba(245, 158, 11, .25);
|
|
1273
1376
|
}
|
|
1274
1377
|
|
|
1378
|
+
/* Operator-only subdomain chip on own published cards */
|
|
1379
|
+
.subdomain-chip {
|
|
1380
|
+
display: inline-flex;
|
|
1381
|
+
align-items: center;
|
|
1382
|
+
gap: 4px;
|
|
1383
|
+
font-size: .7rem;
|
|
1384
|
+
font-weight: 600;
|
|
1385
|
+
padding: 2px 8px;
|
|
1386
|
+
border-radius: 999px;
|
|
1387
|
+
cursor: pointer;
|
|
1388
|
+
background: rgba(99, 102, 241, .10);
|
|
1389
|
+
color: #6366f1;
|
|
1390
|
+
border: 1px solid rgba(99, 102, 241, .25);
|
|
1391
|
+
margin-top: 4px;
|
|
1392
|
+
max-width: 100%;
|
|
1393
|
+
overflow: hidden;
|
|
1394
|
+
text-overflow: ellipsis;
|
|
1395
|
+
white-space: nowrap;
|
|
1396
|
+
}
|
|
1397
|
+
.subdomain-chip:hover { background: rgba(99, 102, 241, .2); }
|
|
1398
|
+
|
|
1399
|
+
/* AI-published origin badge on local grid cards */
|
|
1400
|
+
.ai-origin-badge {
|
|
1401
|
+
display: inline-block;
|
|
1402
|
+
font-size: .6rem;
|
|
1403
|
+
font-weight: 700;
|
|
1404
|
+
text-transform: uppercase;
|
|
1405
|
+
letter-spacing: .04em;
|
|
1406
|
+
padding: 1px 5px;
|
|
1407
|
+
border-radius: 4px;
|
|
1408
|
+
vertical-align: middle;
|
|
1409
|
+
margin-left: 4px;
|
|
1410
|
+
background: rgba(99, 102, 241, .10);
|
|
1411
|
+
color: #6366f1;
|
|
1412
|
+
border: 1px solid rgba(99, 102, 241, .25);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1275
1415
|
/* ── Cortex Extensions Bar ──────────────────────── */
|
|
1276
1416
|
|
|
1277
1417
|
.cortex-mini-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:10px;padding:.75rem 1rem;cursor:pointer;min-width:160px;flex-shrink:0;transition:all .15s}
|
|
@@ -1342,6 +1482,14 @@
|
|
|
1342
1482
|
<button class="icon-btn" id="add-btn" data-i18n-title="action.add" title="Add app">+</button>
|
|
1343
1483
|
<button class="icon-btn" id="generate-btn" data-i18n-title="action.generate" title="Generate with AI">✨</button>
|
|
1344
1484
|
<button class="icon-btn" id="homepage-btn" data-i18n-title="action.homepage" title="Generate Homepage">🏠</button>
|
|
1485
|
+
<div class="backup-menu-wrap">
|
|
1486
|
+
<button class="icon-btn" id="backup-btn" data-i18n-title="backup.menu" title="Backup" onclick="window._launcher.toggleBackupMenu(event)">💾</button>
|
|
1487
|
+
<div id="backup-menu" class="backup-menu" hidden>
|
|
1488
|
+
<button onclick="window._launcher.exportBackupZip()" data-i18n="backup.exportAll">📦 Export all (.zip)</button>
|
|
1489
|
+
<button onclick="window._launcher.importBackupPick()" data-i18n="backup.importFrom">📥 Import from backup...</button>
|
|
1490
|
+
</div>
|
|
1491
|
+
</div>
|
|
1492
|
+
<input type="file" id="backup-file-input" accept=".zip,application/zip,application/x-zip-compressed" hidden />
|
|
1345
1493
|
<div class="lang-toggle" id="lang-toggle" title="Language / Kieli">
|
|
1346
1494
|
<button class="lang-btn" data-lang="en" onclick="window._launcher.setLanguage('en')">EN</button>
|
|
1347
1495
|
<button class="lang-btn" data-lang="fi" onclick="window._launcher.setLanguage('fi')">FI</button>
|
|
@@ -1353,6 +1501,15 @@
|
|
|
1353
1501
|
|
|
1354
1502
|
<nav id="tag-bar"></nav>
|
|
1355
1503
|
|
|
1504
|
+
<!-- Server-published apps missing locally — import is ALWAYS user-selected -->
|
|
1505
|
+
<div id="server-import-banner" class="server-import-banner" style="display:none">
|
|
1506
|
+
<span>📥 <strong id="server-import-count">0</strong> <span data-i18n="srvImport.bannerText">published apps from your account are not in this catalog.</span></span>
|
|
1507
|
+
<div class="server-import-banner-actions">
|
|
1508
|
+
<button class="modal-btn primary" onclick="window._launcher.showServerImportModal()" data-i18n="srvImport.review">Review & import...</button>
|
|
1509
|
+
<button class="modal-btn secondary" onclick="window._launcher.dismissServerImportBanner()" data-i18n="srvImport.dismiss">Not now</button>
|
|
1510
|
+
</div>
|
|
1511
|
+
</div>
|
|
1512
|
+
|
|
1356
1513
|
<!-- Active Extensions Bar -->
|
|
1357
1514
|
<div id="cortex-bar" style="display:none;margin:0 24px 1.5rem;position:relative;z-index:1">
|
|
1358
1515
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem">
|
|
@@ -1577,6 +1734,7 @@
|
|
|
1577
1734
|
<div class="settings-backup-actions">
|
|
1578
1735
|
<button type="button" class="modal-btn secondary" onclick="window._launcher.exportBackup()" data-i18n="settings.exportJson">Export JSON</button>
|
|
1579
1736
|
<button type="button" class="modal-btn secondary" onclick="document.getElementById('import-file-input').click()" data-i18n="settings.importJson">Import JSON</button>
|
|
1737
|
+
<button type="button" class="modal-btn secondary" onclick="window._launcher.removeDuplicateApps()" data-i18n="dedup.btn">🧹 Remove duplicate apps</button>
|
|
1580
1738
|
<input type="file" id="import-file-input" accept=".json" hidden/>
|
|
1581
1739
|
</div>
|
|
1582
1740
|
</div>
|
|
@@ -1641,6 +1799,64 @@
|
|
|
1641
1799
|
</div>
|
|
1642
1800
|
</div>
|
|
1643
1801
|
|
|
1802
|
+
<!-- JSON Backup Import Modal (user-selected, duplicate-aware) -->
|
|
1803
|
+
<div id="json-import-overlay" hidden>
|
|
1804
|
+
<div class="modal" id="json-import-modal">
|
|
1805
|
+
<h2 data-i18n="jsonImport.title">📥 Import JSON backup</h2>
|
|
1806
|
+
<div id="json-import-status" style="margin:4px 0;font-size:.85rem;min-height:1.1em"></div>
|
|
1807
|
+
<div id="json-import-body" style="overflow-y:auto;max-height:55vh"></div>
|
|
1808
|
+
<div class="modal-actions">
|
|
1809
|
+
<button type="button" class="modal-btn secondary" onclick="document.getElementById('json-import-overlay').hidden = true" data-i18n="common.cancel">Cancel</button>
|
|
1810
|
+
<button type="button" class="modal-btn primary" id="json-import-btn" onclick="window._launcher.submitJsonImport()" data-i18n="srvImport.import">Import selected</button>
|
|
1811
|
+
</div>
|
|
1812
|
+
</div>
|
|
1813
|
+
</div>
|
|
1814
|
+
|
|
1815
|
+
<!-- Server Apps Import Modal (user-selected) -->
|
|
1816
|
+
<div id="server-import-overlay" hidden>
|
|
1817
|
+
<div class="modal" id="server-import-modal">
|
|
1818
|
+
<h2 data-i18n="srvImport.title">📥 Import published apps</h2>
|
|
1819
|
+
<div id="server-import-status" style="margin:4px 0;font-size:.85rem;min-height:1.1em"></div>
|
|
1820
|
+
<div id="server-import-body" style="overflow-y:auto;max-height:55vh"></div>
|
|
1821
|
+
<div class="modal-actions">
|
|
1822
|
+
<button type="button" class="modal-btn secondary" onclick="document.getElementById('server-import-overlay').hidden = true" data-i18n="common.cancel">Cancel</button>
|
|
1823
|
+
<button type="button" class="modal-btn primary" onclick="window._launcher.submitServerImport()" data-i18n="srvImport.import">Import selected</button>
|
|
1824
|
+
</div>
|
|
1825
|
+
</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
|
|
1828
|
+
<!-- Backup Import Modal -->
|
|
1829
|
+
<div id="backup-overlay" hidden>
|
|
1830
|
+
<div class="modal" id="backup-modal">
|
|
1831
|
+
<h2 data-i18n="backup.title">📥 Import from backup</h2>
|
|
1832
|
+
<div id="backup-status" style="margin:4px 0;font-size:.85rem;min-height:1.1em"></div>
|
|
1833
|
+
<div id="backup-import-body"></div>
|
|
1834
|
+
<div class="modal-actions">
|
|
1835
|
+
<button type="button" class="modal-btn secondary" onclick="document.getElementById('backup-overlay').hidden = true" data-i18n="common.close">Close</button>
|
|
1836
|
+
<button type="button" class="modal-btn primary" id="backup-restore-btn" onclick="window._launcher.submitBackupRestore()" data-i18n="backup.restore" style="display:none">Restore selected</button>
|
|
1837
|
+
</div>
|
|
1838
|
+
</div>
|
|
1839
|
+
</div>
|
|
1840
|
+
|
|
1841
|
+
<!-- Subdomain Assign Modal (operator only) -->
|
|
1842
|
+
<div id="subdomain-overlay" hidden>
|
|
1843
|
+
<div class="modal" id="subdomain-modal">
|
|
1844
|
+
<h2 data-i18n="subModal.title">🌐 Assign Subdomain</h2>
|
|
1845
|
+
<div style="font-size:.8rem;color:var(--text-muted);margin-bottom:8px" id="subdomain-app-label"></div>
|
|
1846
|
+
<div class="form-group">
|
|
1847
|
+
<label for="subdomain-input" data-i18n="subModal.label">Subdomain</label>
|
|
1848
|
+
<input type="text" id="subdomain-input" class="modal-input" placeholder="sanomat" autocomplete="off"/>
|
|
1849
|
+
<div style="font-size:.75rem;color:var(--text-muted);margin-top:4px" data-i18n="subModal.hint">Lowercase letters, numbers and hyphens (2-63 chars). The app is served at the subdomain root.</div>
|
|
1850
|
+
</div>
|
|
1851
|
+
<div id="subdomain-status" style="margin-top:8px;font-size:.85rem;min-height:1.1em"></div>
|
|
1852
|
+
<div class="modal-actions">
|
|
1853
|
+
<button type="button" class="modal-btn secondary" onclick="document.getElementById('subdomain-overlay').hidden = true" data-i18n="common.cancel">Cancel</button>
|
|
1854
|
+
<button type="button" class="modal-btn secondary" id="subdomain-unassign-btn" onclick="window._launcher.unassignSubdomain()" data-i18n="subModal.unassign">Unassign</button>
|
|
1855
|
+
<button type="button" class="modal-btn primary" id="subdomain-save-btn" onclick="window._launcher.submitSubdomainAssign()" data-i18n="common.save">Save</button>
|
|
1856
|
+
</div>
|
|
1857
|
+
</div>
|
|
1858
|
+
</div>
|
|
1859
|
+
|
|
1644
1860
|
<!-- Versions Modal -->
|
|
1645
1861
|
<div id="versions-overlay" hidden>
|
|
1646
1862
|
<div class="modal" id="versions-modal">
|
|
@@ -1790,6 +2006,63 @@
|
|
|
1790
2006
|
'card.versions': '🕑 Versions',
|
|
1791
2007
|
'card.fork': '⑂ Fork',
|
|
1792
2008
|
'card.restore': '↺ Restore',
|
|
2009
|
+
'card.subdomain': '🌐 Subdomain',
|
|
2010
|
+
'jsonImport.title': '📥 Import JSON backup',
|
|
2011
|
+
'jsonImport.desc': 'Choose which apps to import. Apps already in this catalog are marked as duplicates and unchecked — nothing is imported without your selection.',
|
|
2012
|
+
'jsonImport.statusDup': 'duplicate',
|
|
2013
|
+
'jsonImport.config': 'Also import settings (theme, language, server URL)',
|
|
2014
|
+
'jsonImport.done': 'Imported',
|
|
2015
|
+
'dedup.btn': '🧹 Remove duplicate apps',
|
|
2016
|
+
'dedup.none': 'No duplicate apps found.',
|
|
2017
|
+
'dedup.confirm': 'Delete duplicate copies (favorites and oldest copies are kept)? Copies to delete:',
|
|
2018
|
+
'dedup.done': 'Duplicates removed',
|
|
2019
|
+
'srvImport.bannerText': 'published apps from your account are not in this catalog.',
|
|
2020
|
+
'srvImport.review': '📥 Review & import...',
|
|
2021
|
+
'srvImport.dismiss': 'Not now',
|
|
2022
|
+
'srvImport.title': '📥 Import published apps',
|
|
2023
|
+
'srvImport.desc': 'These apps are published under your account on the server but missing from this local catalog. Choose which ones to import — nothing is imported automatically.',
|
|
2024
|
+
'srvImport.import': 'Import selected',
|
|
2025
|
+
'srvImport.importing': 'Importing...',
|
|
2026
|
+
'backup.menu': 'Backup',
|
|
2027
|
+
'backup.exportAll': '📦 Export all (.zip)',
|
|
2028
|
+
'backup.importFrom': '📥 Import from backup...',
|
|
2029
|
+
'backup.title': '📥 Import from backup',
|
|
2030
|
+
'backup.inspecting': 'Reading backup...',
|
|
2031
|
+
'backup.exporting': 'Building backup...',
|
|
2032
|
+
'backup.from': 'Backup from',
|
|
2033
|
+
'backup.selectAll': 'Select all',
|
|
2034
|
+
'backup.selectNone': 'Select none',
|
|
2035
|
+
'backup.restoring': 'Restoring',
|
|
2036
|
+
'backup.sumApps': 'apps',
|
|
2037
|
+
'backup.sumVersions': 'versions',
|
|
2038
|
+
'backup.sumExts': 'extensions',
|
|
2039
|
+
'backup.colApp': 'App',
|
|
2040
|
+
'backup.colVersions': 'Versions',
|
|
2041
|
+
'backup.colStatus': 'Status',
|
|
2042
|
+
'backup.statusNew': 'new',
|
|
2043
|
+
'backup.statusExists': 'exists',
|
|
2044
|
+
'backup.conflictSkip': 'Skip',
|
|
2045
|
+
'backup.conflictAppend': 'Append versions',
|
|
2046
|
+
'backup.conflictCopy': 'Import as copy',
|
|
2047
|
+
'backup.extensions': '🔌 Extensions',
|
|
2048
|
+
'backup.restore': 'Restore selected',
|
|
2049
|
+
'backup.resultTitle': 'Restore complete',
|
|
2050
|
+
'backup.resCreated': 'Created',
|
|
2051
|
+
'backup.resAppended': 'Versions appended to',
|
|
2052
|
+
'backup.resCopied': 'Imported as copy',
|
|
2053
|
+
'backup.resSkipped': 'Skipped',
|
|
2054
|
+
'backup.resVersions': 'Versions restored',
|
|
2055
|
+
'backup.resErrors': 'Errors',
|
|
2056
|
+
'backup.loginRequired': 'Sign in first to use backups.',
|
|
2057
|
+
'backup.nothingSelected': 'Nothing selected.',
|
|
2058
|
+
'backup.empty': 'The backup contains no apps or extensions.',
|
|
2059
|
+
'subModal.title': '🌐 Assign Subdomain',
|
|
2060
|
+
'subModal.label': 'Subdomain',
|
|
2061
|
+
'subModal.hint': 'Lowercase letters, numbers and hyphens (2-63 chars). The app is served at the subdomain root.',
|
|
2062
|
+
'subModal.unassign': 'Unassign',
|
|
2063
|
+
'subModal.assigned': 'Subdomain assigned',
|
|
2064
|
+
'subModal.unassigned': 'Subdomain unassigned',
|
|
2065
|
+
'subModal.saving': 'Saving...',
|
|
1793
2066
|
'tag.all': 'All',
|
|
1794
2067
|
'tag.favorites': '⭐ Favorites',
|
|
1795
2068
|
'empty.noApps': 'No apps yet',
|
|
@@ -1902,6 +2175,63 @@
|
|
|
1902
2175
|
'card.versions': '🕑 Versiot',
|
|
1903
2176
|
'card.fork': '⑂ Haaroita',
|
|
1904
2177
|
'card.restore': '↺ Palauta',
|
|
2178
|
+
'card.subdomain': '🌐 Alidomain',
|
|
2179
|
+
'jsonImport.title': '📥 Tuo JSON-varmuuskopio',
|
|
2180
|
+
'jsonImport.desc': 'Valitse mitkä appit tuodaan. Katalogissa jo olevat on merkitty duplikaateiksi ja jätetty valitsematta — mitään ei tuoda ilman valintaasi.',
|
|
2181
|
+
'jsonImport.statusDup': 'duplikaatti',
|
|
2182
|
+
'jsonImport.config': 'Tuo myös asetukset (teema, kieli, palvelimen osoite)',
|
|
2183
|
+
'jsonImport.done': 'Tuotu',
|
|
2184
|
+
'dedup.btn': '🧹 Poista duplikaattiappit',
|
|
2185
|
+
'dedup.none': 'Duplikaattiappeja ei löytynyt.',
|
|
2186
|
+
'dedup.confirm': 'Poistetaanko duplikaattikopiot (suosikit ja vanhimmat kopiot säilytetään)? Poistettavia kopioita:',
|
|
2187
|
+
'dedup.done': 'Duplikaatit poistettu',
|
|
2188
|
+
'srvImport.bannerText': 'tilisi julkaistua appia puuttuu tästä katalogista.',
|
|
2189
|
+
'srvImport.review': '📥 Katso ja tuo...',
|
|
2190
|
+
'srvImport.dismiss': 'Ei nyt',
|
|
2191
|
+
'srvImport.title': '📥 Tuo julkaistut appit',
|
|
2192
|
+
'srvImport.desc': 'Nämä appit on julkaistu tililläsi palvelimella mutta puuttuvat paikallisesta katalogista. Valitse mitkä tuodaan — mitään ei tuoda automaattisesti.',
|
|
2193
|
+
'srvImport.import': 'Tuo valitut',
|
|
2194
|
+
'srvImport.importing': 'Tuodaan...',
|
|
2195
|
+
'backup.menu': 'Varmuuskopio',
|
|
2196
|
+
'backup.exportAll': '📦 Vie kaikki (.zip)',
|
|
2197
|
+
'backup.importFrom': '📥 Tuo varmuuskopiosta...',
|
|
2198
|
+
'backup.title': '📥 Tuo varmuuskopiosta',
|
|
2199
|
+
'backup.inspecting': 'Luetaan varmuuskopiota...',
|
|
2200
|
+
'backup.exporting': 'Kootaan varmuuskopiota...',
|
|
2201
|
+
'backup.from': 'Varmuuskopio lähteestä',
|
|
2202
|
+
'backup.selectAll': 'Valitse kaikki',
|
|
2203
|
+
'backup.selectNone': 'Tyhjennä valinta',
|
|
2204
|
+
'backup.restoring': 'Palautetaan',
|
|
2205
|
+
'backup.sumApps': 'appia',
|
|
2206
|
+
'backup.sumVersions': 'versiota',
|
|
2207
|
+
'backup.sumExts': 'laajennusta',
|
|
2208
|
+
'backup.colApp': 'Appi',
|
|
2209
|
+
'backup.colVersions': 'Versiot',
|
|
2210
|
+
'backup.colStatus': 'Tila',
|
|
2211
|
+
'backup.statusNew': 'uusi',
|
|
2212
|
+
'backup.statusExists': 'olemassa',
|
|
2213
|
+
'backup.conflictSkip': 'Ohita',
|
|
2214
|
+
'backup.conflictAppend': 'Lisää versiot päälle',
|
|
2215
|
+
'backup.conflictCopy': 'Tuo kopiona',
|
|
2216
|
+
'backup.extensions': '🔌 Laajennukset',
|
|
2217
|
+
'backup.restore': 'Palauta valitut',
|
|
2218
|
+
'backup.resultTitle': 'Palautus valmis',
|
|
2219
|
+
'backup.resCreated': 'Luotiin',
|
|
2220
|
+
'backup.resAppended': 'Versiot lisätty päälle',
|
|
2221
|
+
'backup.resCopied': 'Tuotu kopiona',
|
|
2222
|
+
'backup.resSkipped': 'Ohitettiin',
|
|
2223
|
+
'backup.resVersions': 'Versioita palautettu',
|
|
2224
|
+
'backup.resErrors': 'Virheet',
|
|
2225
|
+
'backup.loginRequired': 'Kirjaudu ensin sisään käyttääksesi varmuuskopioita.',
|
|
2226
|
+
'backup.nothingSelected': 'Mitään ei ole valittu.',
|
|
2227
|
+
'backup.empty': 'Varmuuskopiossa ei ole appeja eikä laajennuksia.',
|
|
2228
|
+
'subModal.title': '🌐 Liitä alidomain',
|
|
2229
|
+
'subModal.label': 'Alidomain',
|
|
2230
|
+
'subModal.hint': 'Pienet kirjaimet, numerot ja viivat (2-63 merkkiä). Appi tarjoillaan alidomainin juuresta.',
|
|
2231
|
+
'subModal.unassign': 'Poista liitos',
|
|
2232
|
+
'subModal.assigned': 'Alidomain liitetty',
|
|
2233
|
+
'subModal.unassigned': 'Liitos poistettu',
|
|
2234
|
+
'subModal.saving': 'Tallennetaan...',
|
|
1905
2235
|
'tag.all': 'Kaikki',
|
|
1906
2236
|
'tag.favorites': '⭐ Suosikit',
|
|
1907
2237
|
'empty.noApps': 'Ei vielä sovelluksia',
|
|
@@ -2813,7 +3143,8 @@
|
|
|
2813
3143
|
'<button class="card-menu-btn" onclick="event.stopPropagation(); window._launcher.showContextMenu(event, \'' + escapeHtml(app.id) + '\')" title="Menu">\u22EE</button>' +
|
|
2814
3144
|
'<div class="app-icon">' + escapeHtml(iconDisplay) + '</div>' +
|
|
2815
3145
|
'<div class="app-name">' + escapeHtml(app.name) + '</div>' +
|
|
2816
|
-
'<div class="app-source">' + sourceLabel(app.source) +
|
|
3146
|
+
'<div class="app-source">' + sourceLabel(app.source) +
|
|
3147
|
+
(app.origin === 'ai-published' ? ' <span class="ai-origin-badge">AI</span>' : '') + '</div>' +
|
|
2817
3148
|
'<div class="app-actions">' +
|
|
2818
3149
|
'<button onclick="event.stopPropagation(); window._launcher.launchApp(\'' + escapeHtml(app.id) + '\', \'tab\')" title="Open in tab">\u25B6 Tab</button>' +
|
|
2819
3150
|
'<button onclick="event.stopPropagation(); window._launcher.launchApp(\'' + escapeHtml(app.id) + '\', \'iframe\')" title="Open in iframe">\u25FB</button>' +
|
|
@@ -2995,6 +3326,9 @@
|
|
|
2995
3326
|
|
|
2996
3327
|
case 'delete':
|
|
2997
3328
|
if (confirm('Delete "' + (app.name || 'this app') + '"?')) {
|
|
3329
|
+
// Tombstone published apps so the server copy isn't re-imported as a
|
|
3330
|
+
// "new" AI-published app on the next load.
|
|
3331
|
+
if (app.publishedFilename) addImportIgnore(app.publishedFilename);
|
|
2998
3332
|
deleteApp(appId).then(function () {
|
|
2999
3333
|
renderApps();
|
|
3000
3334
|
loadPublishedApps();
|
|
@@ -3284,41 +3618,147 @@
|
|
|
3284
3618
|
});
|
|
3285
3619
|
}
|
|
3286
3620
|
|
|
3621
|
+
// Identity key for duplicate detection: a published app is the same app
|
|
3622
|
+
// regardless of its local id; unpublished ones match by name + content size.
|
|
3623
|
+
function localAppKey(a) {
|
|
3624
|
+
if (a.publishedFilename) return 'pf:' + a.publishedFilename;
|
|
3625
|
+
return 'nm:' + (a.name || '') + '|' + (a.blob ? a.blob.length : (a.url || ''));
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// Parsed JSON backup awaiting user selection: { backup, rows: [{app, isDup}] }
|
|
3629
|
+
var jsonImportState = null;
|
|
3630
|
+
|
|
3287
3631
|
function handleImportBackup(file) {
|
|
3288
3632
|
if (!file) return;
|
|
3289
3633
|
var reader = new FileReader();
|
|
3290
3634
|
reader.onload = function () {
|
|
3635
|
+
var backup;
|
|
3291
3636
|
try {
|
|
3292
|
-
|
|
3293
|
-
if (!backup.apps || !Array.isArray(backup.apps)) {
|
|
3294
|
-
alert('Invalid backup file: missing apps array');
|
|
3295
|
-
return;
|
|
3296
|
-
}
|
|
3297
|
-
// Import config if present
|
|
3298
|
-
if (backup.config && typeof backup.config === 'object') {
|
|
3299
|
-
var currentConfig = loadConfig();
|
|
3300
|
-
var merged = Object.assign({}, currentConfig, backup.config);
|
|
3301
|
-
saveConfig(merged);
|
|
3302
|
-
applyTheme(merged.theme);
|
|
3303
|
-
}
|
|
3304
|
-
// Import apps with new IDs to avoid conflicts
|
|
3305
|
-
var promises = [];
|
|
3306
|
-
for (var i = 0; i < backup.apps.length; i++) {
|
|
3307
|
-
var app = Object.assign({}, backup.apps[i]);
|
|
3308
|
-
app.id = generateId(); // New ID to avoid conflicts
|
|
3309
|
-
promises.push(saveApp(app));
|
|
3310
|
-
}
|
|
3311
|
-
Promise.all(promises).then(function () {
|
|
3312
|
-
alert('Imported ' + backup.apps.length + ' app(s) successfully');
|
|
3313
|
-
renderApps();
|
|
3314
|
-
});
|
|
3637
|
+
backup = JSON.parse(reader.result);
|
|
3315
3638
|
} catch (e) {
|
|
3316
3639
|
alert('Failed to parse backup file: ' + (e.message || e));
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
if (!backup.apps || !Array.isArray(backup.apps)) {
|
|
3643
|
+
alert('Invalid backup file: missing apps array');
|
|
3644
|
+
return;
|
|
3317
3645
|
}
|
|
3646
|
+
// Inspect-before-write: compare against the current catalog and let the
|
|
3647
|
+
// user choose. Duplicates default to unchecked — nothing is imported
|
|
3648
|
+
// (and no setting is changed) without an explicit selection.
|
|
3649
|
+
getAllApps().then(function (existing) {
|
|
3650
|
+
var existingKeys = {};
|
|
3651
|
+
existing.forEach(function (a) { existingKeys[localAppKey(a)] = true; });
|
|
3652
|
+
jsonImportState = {
|
|
3653
|
+
backup: backup,
|
|
3654
|
+
rows: backup.apps.map(function (a) {
|
|
3655
|
+
return { app: a, isDup: !!existingKeys[localAppKey(a)] };
|
|
3656
|
+
}),
|
|
3657
|
+
};
|
|
3658
|
+
renderJsonImportModal();
|
|
3659
|
+
});
|
|
3318
3660
|
};
|
|
3319
3661
|
reader.readAsText(file);
|
|
3320
3662
|
}
|
|
3321
3663
|
|
|
3664
|
+
function renderJsonImportModal() {
|
|
3665
|
+
var rows = jsonImportState.rows;
|
|
3666
|
+
var body = document.getElementById('json-import-body');
|
|
3667
|
+
var html = '<p style="font-size:.8rem;color:var(--text-muted);margin:4px 0 8px">' + t('jsonImport.desc') + '</p>' +
|
|
3668
|
+
'<div class="backup-toolbar-row"><div>' +
|
|
3669
|
+
'<button class="backup-link-btn" onclick="window._launcher.jsonImportSelectAll(true)">' + t('backup.selectAll') + '</button> / ' +
|
|
3670
|
+
'<button class="backup-link-btn" onclick="window._launcher.jsonImportSelectAll(false)">' + t('backup.selectNone') + '</button>' +
|
|
3671
|
+
'</div></div>' +
|
|
3672
|
+
'<table class="backup-table"><tbody>';
|
|
3673
|
+
for (var i = 0; i < rows.length; i++) {
|
|
3674
|
+
var a = rows[i].app;
|
|
3675
|
+
html +=
|
|
3676
|
+
'<tr>' +
|
|
3677
|
+
'<td><input type="checkbox" class="json-import-cb" data-i="' + i + '"' + (rows[i].isDup ? '' : ' checked') + '/></td>' +
|
|
3678
|
+
'<td>' + escapeHtml(a.name || a.publishedFilename || 'Untitled') +
|
|
3679
|
+
(a.publishedFilename ? '<div class="mono-cell">' + escapeHtml(a.publishedFilename) + '</div>' : '') + '</td>' +
|
|
3680
|
+
'<td>' + sourceLabel(a.source) + '</td>' +
|
|
3681
|
+
'<td>' + (rows[i].isDup
|
|
3682
|
+
? '<span class="backup-status-badge backup-status-exists">' + t('jsonImport.statusDup') + '</span>'
|
|
3683
|
+
: '<span class="backup-status-badge backup-status-new">' + t('backup.statusNew') + '</span>') + '</td>' +
|
|
3684
|
+
'</tr>';
|
|
3685
|
+
}
|
|
3686
|
+
html += '</tbody></table>';
|
|
3687
|
+
if (jsonImportState.backup.config && typeof jsonImportState.backup.config === 'object') {
|
|
3688
|
+
html += '<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-size:.85rem">' +
|
|
3689
|
+
'<input type="checkbox" id="json-import-config"/> ' + t('jsonImport.config') + '</label>';
|
|
3690
|
+
}
|
|
3691
|
+
body.innerHTML = html;
|
|
3692
|
+
document.getElementById('json-import-status').textContent = '';
|
|
3693
|
+
document.getElementById('json-import-btn').style.display = '';
|
|
3694
|
+
document.getElementById('json-import-overlay').hidden = false;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function jsonImportSelectAll(checked) {
|
|
3698
|
+
document.querySelectorAll('.json-import-cb').forEach(function (b) { b.checked = checked; });
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
function submitJsonImport() {
|
|
3702
|
+
var statusEl = document.getElementById('json-import-status');
|
|
3703
|
+
var selected = [];
|
|
3704
|
+
document.querySelectorAll('.json-import-cb').forEach(function (b) {
|
|
3705
|
+
if (b.checked) selected.push(jsonImportState.rows[parseInt(b.getAttribute('data-i'), 10)].app);
|
|
3706
|
+
});
|
|
3707
|
+
var configCb = document.getElementById('json-import-config');
|
|
3708
|
+
var importConfig = !!(configCb && configCb.checked);
|
|
3709
|
+
if (selected.length === 0 && !importConfig) {
|
|
3710
|
+
statusEl.style.color = '#ef4444';
|
|
3711
|
+
statusEl.textContent = t('backup.nothingSelected');
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
if (importConfig) {
|
|
3715
|
+
var merged = Object.assign({}, loadConfig(), jsonImportState.backup.config);
|
|
3716
|
+
saveConfig(merged);
|
|
3717
|
+
applyTheme(merged.theme);
|
|
3718
|
+
}
|
|
3719
|
+
Promise.all(selected.map(function (a) {
|
|
3720
|
+
var app = Object.assign({}, a);
|
|
3721
|
+
app.id = generateId(); // new id — the source backup may collide with existing ids
|
|
3722
|
+
return saveApp(app);
|
|
3723
|
+
})).then(function () {
|
|
3724
|
+
statusEl.style.color = '#34d399';
|
|
3725
|
+
statusEl.textContent = '✔ ' + t('jsonImport.done') + ': ' + selected.length;
|
|
3726
|
+
document.getElementById('json-import-btn').style.display = 'none';
|
|
3727
|
+
renderApps();
|
|
3728
|
+
loadPublishedApps();
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// ── Remove duplicate apps (cleanup after legacy blind imports) ──
|
|
3733
|
+
|
|
3734
|
+
function removeDuplicateApps() {
|
|
3735
|
+
getAllApps().then(function (apps) {
|
|
3736
|
+
var groups = {};
|
|
3737
|
+
apps.forEach(function (a) {
|
|
3738
|
+
var k = localAppKey(a);
|
|
3739
|
+
(groups[k] = groups[k] || []).push(a);
|
|
3740
|
+
});
|
|
3741
|
+
var toDelete = [];
|
|
3742
|
+
Object.keys(groups).forEach(function (k) {
|
|
3743
|
+
var g = groups[k];
|
|
3744
|
+
if (g.length < 2) return;
|
|
3745
|
+
// Keep the best copy: favorites first, then the earliest added
|
|
3746
|
+
g.sort(function (x, y) {
|
|
3747
|
+
if (!!y.favorite !== !!x.favorite) return (y.favorite ? 1 : 0) - (x.favorite ? 1 : 0);
|
|
3748
|
+
return (x.addedAt || '') < (y.addedAt || '') ? -1 : 1;
|
|
3749
|
+
});
|
|
3750
|
+
toDelete = toDelete.concat(g.slice(1));
|
|
3751
|
+
});
|
|
3752
|
+
if (toDelete.length === 0) { alert(t('dedup.none')); return; }
|
|
3753
|
+
if (!confirm(t('dedup.confirm') + ' ' + toDelete.length)) return;
|
|
3754
|
+
Promise.all(toDelete.map(function (a) { return deleteApp(a.id); })).then(function () {
|
|
3755
|
+
renderApps();
|
|
3756
|
+
loadPublishedApps();
|
|
3757
|
+
alert(t('dedup.done') + ': ' + toDelete.length);
|
|
3758
|
+
});
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3322
3762
|
// ── Clear All Data ──────────────────────────────
|
|
3323
3763
|
|
|
3324
3764
|
function clearAllData() {
|
|
@@ -3663,6 +4103,591 @@
|
|
|
3663
4103
|
return !!a && a === bareOwnerName(sessionOwner);
|
|
3664
4104
|
}
|
|
3665
4105
|
|
|
4106
|
+
// ── Operator subdomain mappings ─────────────────
|
|
4107
|
+
// Operators can map a subdomain to a published app (served at the subdomain
|
|
4108
|
+
// root). Mappings are loaded only for operator sessions; everyone else never
|
|
4109
|
+
// sees the controls and the admin API returns 403 anyway.
|
|
4110
|
+
|
|
4111
|
+
function getSessionRoles() {
|
|
4112
|
+
try {
|
|
4113
|
+
var token = getCortexOwnerToken();
|
|
4114
|
+
if (!token) return [];
|
|
4115
|
+
var payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
4116
|
+
return payload.roles || [];
|
|
4117
|
+
} catch (e) { return []; }
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
function isOperatorSession() {
|
|
4121
|
+
return getSessionRoles().indexOf('operator') !== -1;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
// target ("owner/filename") → site record; null until loaded
|
|
4125
|
+
var subdomainsByTarget = null;
|
|
4126
|
+
|
|
4127
|
+
function loadSubdomainSites() {
|
|
4128
|
+
if (!isOperatorSession()) {
|
|
4129
|
+
subdomainsByTarget = null;
|
|
4130
|
+
return Promise.resolve();
|
|
4131
|
+
}
|
|
4132
|
+
var config = loadConfig();
|
|
4133
|
+
var aimeatUrl = config.aimeatUrl ? config.aimeatUrl.replace(/\/+$/, '') : '';
|
|
4134
|
+
if (!aimeatUrl) { subdomainsByTarget = null; return Promise.resolve(); }
|
|
4135
|
+
return fetch(aimeatUrl + '/v1/admin/subdomains', {
|
|
4136
|
+
headers: { 'Authorization': 'Bearer ' + getCortexOwnerToken() }
|
|
4137
|
+
})
|
|
4138
|
+
.then(function (resp) { if (!resp.ok) throw new Error('HTTP ' + resp.status); return resp.json(); })
|
|
4139
|
+
.then(function (json) {
|
|
4140
|
+
subdomainsByTarget = {};
|
|
4141
|
+
var sites = (json.data && json.data.sites) || [];
|
|
4142
|
+
for (var i = 0; i < sites.length; i++) {
|
|
4143
|
+
if (sites[i].kind === 'app') subdomainsByTarget[sites[i].target] = sites[i];
|
|
4144
|
+
}
|
|
4145
|
+
})
|
|
4146
|
+
.catch(function () { subdomainsByTarget = null; });
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
function apexHostLabel() {
|
|
4150
|
+
try {
|
|
4151
|
+
var config = loadConfig();
|
|
4152
|
+
return new URL(config.aimeatUrl || window.location.origin).host;
|
|
4153
|
+
} catch (e) { return window.location.host; }
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
// The app target currently open in the subdomain modal: { target, existingSub }
|
|
4157
|
+
var subdomainModalState = null;
|
|
4158
|
+
|
|
4159
|
+
function showSubdomainModal(owner, filename) {
|
|
4160
|
+
var target = bareOwnerName(owner) + '/' + filename;
|
|
4161
|
+
var existing = (subdomainsByTarget && subdomainsByTarget[target]) || null;
|
|
4162
|
+
subdomainModalState = { target: target, existingSub: existing ? existing.subdomain : null };
|
|
4163
|
+
document.getElementById('subdomain-app-label').textContent = target;
|
|
4164
|
+
document.getElementById('subdomain-input').value = existing ? existing.subdomain : '';
|
|
4165
|
+
document.getElementById('subdomain-status').textContent = '';
|
|
4166
|
+
document.getElementById('subdomain-unassign-btn').style.display = existing ? '' : 'none';
|
|
4167
|
+
document.getElementById('subdomain-overlay').hidden = false;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function subdomainApiCall(method, path, body) {
|
|
4171
|
+
var config = loadConfig();
|
|
4172
|
+
var aimeatUrl = config.aimeatUrl.replace(/\/+$/, '');
|
|
4173
|
+
return fetch(aimeatUrl + path, {
|
|
4174
|
+
method: method,
|
|
4175
|
+
headers: {
|
|
4176
|
+
'Authorization': 'Bearer ' + getCortexOwnerToken(),
|
|
4177
|
+
'Content-Type': 'application/json'
|
|
4178
|
+
},
|
|
4179
|
+
body: body ? JSON.stringify(body) : undefined
|
|
4180
|
+
}).then(function (resp) {
|
|
4181
|
+
return resp.json().then(function (json) {
|
|
4182
|
+
if (!json.ok) {
|
|
4183
|
+
var msg = (json.error && (json.error.message || json.error.code)) || ('HTTP ' + resp.status);
|
|
4184
|
+
throw new Error(msg);
|
|
4185
|
+
}
|
|
4186
|
+
return json;
|
|
4187
|
+
});
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
function submitSubdomainAssign() {
|
|
4192
|
+
if (!subdomainModalState) return;
|
|
4193
|
+
var sub = document.getElementById('subdomain-input').value.trim().toLowerCase();
|
|
4194
|
+
var statusEl = document.getElementById('subdomain-status');
|
|
4195
|
+
if (!sub) return;
|
|
4196
|
+
if (sub === subdomainModalState.existingSub) {
|
|
4197
|
+
document.getElementById('subdomain-overlay').hidden = true;
|
|
4198
|
+
return;
|
|
4199
|
+
}
|
|
4200
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
4201
|
+
statusEl.textContent = t('subModal.saving');
|
|
4202
|
+
var oldSub = subdomainModalState.existingSub;
|
|
4203
|
+
subdomainApiCall('POST', '/v1/admin/subdomains', {
|
|
4204
|
+
subdomain: sub, kind: 'app', target: subdomainModalState.target
|
|
4205
|
+
})
|
|
4206
|
+
.then(function () {
|
|
4207
|
+
// Re-pointing to a new subdomain: drop the old mapping after the new
|
|
4208
|
+
// one exists, so the app is never left unmapped on failure.
|
|
4209
|
+
if (oldSub) return subdomainApiCall('DELETE', '/v1/admin/subdomains/' + encodeURIComponent(oldSub));
|
|
4210
|
+
})
|
|
4211
|
+
.then(function () {
|
|
4212
|
+
statusEl.style.color = '#34d399';
|
|
4213
|
+
statusEl.textContent = '✔ ' + t('subModal.assigned') + ' — ' + sub + '.' + apexHostLabel();
|
|
4214
|
+
return loadSubdomainSites();
|
|
4215
|
+
})
|
|
4216
|
+
.then(function () { loadPublishedApps(); })
|
|
4217
|
+
.catch(function (err) {
|
|
4218
|
+
statusEl.style.color = '#ef4444';
|
|
4219
|
+
statusEl.textContent = err.message || String(err);
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
function unassignSubdomain() {
|
|
4224
|
+
if (!subdomainModalState || !subdomainModalState.existingSub) return;
|
|
4225
|
+
var statusEl = document.getElementById('subdomain-status');
|
|
4226
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
4227
|
+
statusEl.textContent = t('subModal.saving');
|
|
4228
|
+
subdomainApiCall('DELETE', '/v1/admin/subdomains/' + encodeURIComponent(subdomainModalState.existingSub))
|
|
4229
|
+
.then(function () {
|
|
4230
|
+
statusEl.style.color = '#34d399';
|
|
4231
|
+
statusEl.textContent = '✔ ' + t('subModal.unassigned');
|
|
4232
|
+
document.getElementById('subdomain-unassign-btn').style.display = 'none';
|
|
4233
|
+
subdomainModalState.existingSub = null;
|
|
4234
|
+
return loadSubdomainSites();
|
|
4235
|
+
})
|
|
4236
|
+
.then(function () { loadPublishedApps(); })
|
|
4237
|
+
.catch(function (err) {
|
|
4238
|
+
statusEl.style.color = '#ef4444';
|
|
4239
|
+
statusEl.textContent = err.message || String(err);
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
// ── Backup: export all + selective import ────────
|
|
4244
|
+
// Follows the organism-export bundle model: one ZIP, manifest + per-item
|
|
4245
|
+
// folders, inspect-before-write, explicit conflict modes — never a silent
|
|
4246
|
+
// overwrite.
|
|
4247
|
+
|
|
4248
|
+
function toggleBackupMenu(event) {
|
|
4249
|
+
if (event && event.stopPropagation) event.stopPropagation();
|
|
4250
|
+
var menu = document.getElementById('backup-menu');
|
|
4251
|
+
menu.hidden = !menu.hidden;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
function backupApiBase() {
|
|
4255
|
+
var config = loadConfig();
|
|
4256
|
+
return (config.aimeatUrl || window.location.origin).replace(/\/+$/, '');
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
function exportBackupZip() {
|
|
4260
|
+
document.getElementById('backup-menu').hidden = true;
|
|
4261
|
+
var token = getCortexOwnerToken();
|
|
4262
|
+
if (!token) { alert(t('backup.loginRequired')); return; }
|
|
4263
|
+
var btn = document.getElementById('backup-btn');
|
|
4264
|
+
btn.disabled = true;
|
|
4265
|
+
fetch(backupApiBase() + '/v1/apps/backup', { headers: { 'Authorization': 'Bearer ' + token } })
|
|
4266
|
+
.then(function (r) {
|
|
4267
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
4268
|
+
var cd = r.headers.get('Content-Disposition') || '';
|
|
4269
|
+
var m = cd.match(/filename="([^"]+)"/);
|
|
4270
|
+
return r.blob().then(function (b) { return { blob: b, name: m ? m[1] : 'aimeat-apps-backup.zip' }; });
|
|
4271
|
+
})
|
|
4272
|
+
.then(function (o) {
|
|
4273
|
+
var a = document.createElement('a');
|
|
4274
|
+
a.href = URL.createObjectURL(o.blob);
|
|
4275
|
+
a.download = o.name;
|
|
4276
|
+
a.click();
|
|
4277
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); }, 5000);
|
|
4278
|
+
})
|
|
4279
|
+
.catch(function (e) { alert('Export failed: ' + (e.message || e)); })
|
|
4280
|
+
.finally(function () { btn.disabled = false; });
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
var backupInspectData = null; // inspect response (apps, extensions, backup_token)
|
|
4284
|
+
|
|
4285
|
+
function importBackupPick() {
|
|
4286
|
+
document.getElementById('backup-menu').hidden = true;
|
|
4287
|
+
var token = getCortexOwnerToken();
|
|
4288
|
+
if (!token) { alert(t('backup.loginRequired')); return; }
|
|
4289
|
+
document.getElementById('backup-file-input').click();
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function importBackupFile(file) {
|
|
4293
|
+
var overlay = document.getElementById('backup-overlay');
|
|
4294
|
+
var statusEl = document.getElementById('backup-status');
|
|
4295
|
+
var body = document.getElementById('backup-import-body');
|
|
4296
|
+
document.getElementById('backup-restore-btn').style.display = 'none';
|
|
4297
|
+
body.innerHTML = '';
|
|
4298
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
4299
|
+
statusEl.textContent = t('backup.inspecting');
|
|
4300
|
+
overlay.hidden = false;
|
|
4301
|
+
|
|
4302
|
+
file.arrayBuffer()
|
|
4303
|
+
.then(function (buf) {
|
|
4304
|
+
return fetch(backupApiBase() + '/v1/apps/backup/inspect', {
|
|
4305
|
+
method: 'POST',
|
|
4306
|
+
headers: {
|
|
4307
|
+
'Authorization': 'Bearer ' + getCortexOwnerToken(),
|
|
4308
|
+
'Content-Type': 'application/zip'
|
|
4309
|
+
},
|
|
4310
|
+
body: buf
|
|
4311
|
+
});
|
|
4312
|
+
})
|
|
4313
|
+
.then(function (r) { return r.json().then(function (j) { return { status: r.status, json: j }; }); })
|
|
4314
|
+
.then(function (o) {
|
|
4315
|
+
if (!o.json.ok) {
|
|
4316
|
+
var msg = (o.json.error && (o.json.error.message || o.json.error.code)) || ('HTTP ' + o.status);
|
|
4317
|
+
throw new Error(msg);
|
|
4318
|
+
}
|
|
4319
|
+
backupInspectData = o.json.data;
|
|
4320
|
+
statusEl.textContent = '';
|
|
4321
|
+
renderBackupSelection();
|
|
4322
|
+
})
|
|
4323
|
+
.catch(function (e) {
|
|
4324
|
+
statusEl.style.color = '#ef4444';
|
|
4325
|
+
statusEl.textContent = e.message || String(e);
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
function renderBackupSelection() {
|
|
4330
|
+
var d = backupInspectData;
|
|
4331
|
+
var body = document.getElementById('backup-import-body');
|
|
4332
|
+
if (!d || (d.apps.length === 0 && d.extensions.length === 0)) {
|
|
4333
|
+
body.innerHTML = '<p style="color:var(--text-muted)">' + t('backup.empty') + '</p>';
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
var conflictOptions =
|
|
4338
|
+
'<option value="skip">' + t('backup.conflictSkip') + '</option>' +
|
|
4339
|
+
'<option value="append">' + t('backup.conflictAppend') + '</option>' +
|
|
4340
|
+
'<option value="copy">' + t('backup.conflictCopy') + '</option>';
|
|
4341
|
+
|
|
4342
|
+
var html =
|
|
4343
|
+
'<div style="font-size:.8rem;color:var(--text-muted)">' + t('backup.from') + ': <span style="font-family:monospace">' +
|
|
4344
|
+
escapeHtml((d.source.owner || '?') + '@' + (d.source.nodeId || '?')) + '</span>' +
|
|
4345
|
+
(d.exported_at ? ' · ' + new Date(d.exported_at).toLocaleString() : '') +
|
|
4346
|
+
'</div>' +
|
|
4347
|
+
'<div class="backup-toolbar-row">' +
|
|
4348
|
+
'<div>' +
|
|
4349
|
+
'<button class="backup-link-btn" onclick="window._launcher.backupSelectAll(true)">' + t('backup.selectAll') + '</button> / ' +
|
|
4350
|
+
'<button class="backup-link-btn" onclick="window._launcher.backupSelectAll(false)">' + t('backup.selectNone') + '</button>' +
|
|
4351
|
+
'</div>' +
|
|
4352
|
+
'<div class="backup-summary-line" id="backup-summary"></div>' +
|
|
4353
|
+
'</div>' +
|
|
4354
|
+
'<table class="backup-table"><thead><tr>' +
|
|
4355
|
+
'<th></th><th>' + t('backup.colApp') + '</th><th>' + t('backup.colVersions') + '</th><th>' + t('backup.colStatus') + '</th>' +
|
|
4356
|
+
'</tr></thead><tbody>';
|
|
4357
|
+
|
|
4358
|
+
for (var i = 0; i < d.apps.length; i++) {
|
|
4359
|
+
var app = d.apps[i];
|
|
4360
|
+
var versionList = '';
|
|
4361
|
+
for (var v = 0; v < app.versions.length; v++) {
|
|
4362
|
+
var ver = app.versions[v];
|
|
4363
|
+
versionList +=
|
|
4364
|
+
'<label><input type="checkbox" class="backup-ver" data-app="' + i + '" data-v="' + ver.version + '" checked ' +
|
|
4365
|
+
'onchange="window._launcher.backupUpdateSummary()"/> v' + ver.version +
|
|
4366
|
+
(ver.semver ? ' (' + escapeHtml(ver.semver) + ')' : '') +
|
|
4367
|
+
' · ' + (ver.size < 1024 ? ver.size + ' B' : (ver.size / 1024).toFixed(1) + ' KB') +
|
|
4368
|
+
(ver.created_at ? ' · ' + new Date(ver.created_at).toLocaleDateString() : '') +
|
|
4369
|
+
'</label>';
|
|
4370
|
+
}
|
|
4371
|
+
html +=
|
|
4372
|
+
'<tr>' +
|
|
4373
|
+
'<td><input type="checkbox" id="backup-app-' + i + '" checked onchange="window._launcher.backupUpdateSummary()"/></td>' +
|
|
4374
|
+
'<td>' + escapeHtml(app.name || app.filename) +
|
|
4375
|
+
'<div class="mono-cell">' + escapeHtml(app.filename) + '</div></td>' +
|
|
4376
|
+
'<td>' + app.versions.length + ' ' +
|
|
4377
|
+
'<button class="backup-versions-toggle" onclick="var el=document.getElementById(\'backup-app-' + i + '-versions\');el.style.display=el.style.display===\'none\'?\'\':\'none\'">' + t('backup.colVersions').toLowerCase() + ' ▾</button>' +
|
|
4378
|
+
'<div class="backup-version-list" id="backup-app-' + i + '-versions" style="display:none">' + versionList + '</div></td>' +
|
|
4379
|
+
'<td>' +
|
|
4380
|
+
(app.exists
|
|
4381
|
+
? '<span class="backup-status-badge backup-status-exists">' + t('backup.statusExists') + '</span><br/>' +
|
|
4382
|
+
'<select class="backup-conflict-select" id="backup-app-' + i + '-conflict">' + conflictOptions + '</select>'
|
|
4383
|
+
: '<span class="backup-status-badge backup-status-new">' + t('backup.statusNew') + '</span>') +
|
|
4384
|
+
'</td>' +
|
|
4385
|
+
'</tr>';
|
|
4386
|
+
}
|
|
4387
|
+
html += '</tbody></table>';
|
|
4388
|
+
|
|
4389
|
+
if (d.extensions.length > 0) {
|
|
4390
|
+
html += '<h3 style="font-size:.9rem;margin:14px 0 6px">' + t('backup.extensions') + '</h3>';
|
|
4391
|
+
for (var e = 0; e < d.extensions.length; e++) {
|
|
4392
|
+
var ext = d.extensions[e];
|
|
4393
|
+
html +=
|
|
4394
|
+
'<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;font-size:.85rem">' +
|
|
4395
|
+
'<input type="checkbox" id="backup-ext-' + e + '" checked onchange="window._launcher.backupUpdateSummary()"/>' +
|
|
4396
|
+
'<span style="font-family:monospace">' + escapeHtml(ext.name) + '</span>' +
|
|
4397
|
+
(ext.exists
|
|
4398
|
+
? '<span class="backup-status-badge backup-status-exists">' + t('backup.statusExists') + '</span>' +
|
|
4399
|
+
'<select class="backup-conflict-select" id="backup-ext-' + e + '-conflict">' +
|
|
4400
|
+
'<option value="skip">' + t('backup.conflictSkip') + '</option>' +
|
|
4401
|
+
'<option value="copy">' + t('backup.conflictCopy') + '</option>' +
|
|
4402
|
+
'</select>'
|
|
4403
|
+
: '<span class="backup-status-badge backup-status-new">' + t('backup.statusNew') + '</span>') +
|
|
4404
|
+
'</div>';
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
body.innerHTML = html;
|
|
4409
|
+
document.getElementById('backup-restore-btn').style.display = '';
|
|
4410
|
+
backupUpdateSummary();
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
// Reads the current selection straight from the DOM — no parallel state to drift.
|
|
4414
|
+
function backupCollectSelections() {
|
|
4415
|
+
var d = backupInspectData;
|
|
4416
|
+
var selections = [];
|
|
4417
|
+
var extensions = [];
|
|
4418
|
+
if (!d) return { selections: selections, extensions: extensions, versionTotal: 0 };
|
|
4419
|
+
var versionTotal = 0;
|
|
4420
|
+
for (var i = 0; i < d.apps.length; i++) {
|
|
4421
|
+
var cb = document.getElementById('backup-app-' + i);
|
|
4422
|
+
if (!cb || !cb.checked) continue;
|
|
4423
|
+
var app = d.apps[i];
|
|
4424
|
+
var checkedVers = [];
|
|
4425
|
+
var verBoxes = document.querySelectorAll('.backup-ver[data-app="' + i + '"]');
|
|
4426
|
+
verBoxes.forEach(function (b) { if (b.checked) checkedVers.push(parseInt(b.getAttribute('data-v'), 10)); });
|
|
4427
|
+
if (checkedVers.length === 0) continue;
|
|
4428
|
+
var sel = { filename: app.filename };
|
|
4429
|
+
if (checkedVers.length !== app.versions.length) sel.versions = checkedVers;
|
|
4430
|
+
if (app.exists) {
|
|
4431
|
+
var conflictEl = document.getElementById('backup-app-' + i + '-conflict');
|
|
4432
|
+
sel.conflict = conflictEl ? conflictEl.value : 'skip';
|
|
4433
|
+
}
|
|
4434
|
+
versionTotal += checkedVers.length;
|
|
4435
|
+
selections.push(sel);
|
|
4436
|
+
}
|
|
4437
|
+
for (var e = 0; e < d.extensions.length; e++) {
|
|
4438
|
+
var ecb = document.getElementById('backup-ext-' + e);
|
|
4439
|
+
if (!ecb || !ecb.checked) continue;
|
|
4440
|
+
var ext = { name: d.extensions[e].name };
|
|
4441
|
+
if (d.extensions[e].exists) {
|
|
4442
|
+
var ecEl = document.getElementById('backup-ext-' + e + '-conflict');
|
|
4443
|
+
ext.conflict = ecEl ? ecEl.value : 'skip';
|
|
4444
|
+
}
|
|
4445
|
+
extensions.push(ext);
|
|
4446
|
+
}
|
|
4447
|
+
return { selections: selections, extensions: extensions, versionTotal: versionTotal };
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
function backupUpdateSummary() {
|
|
4451
|
+
var sel = backupCollectSelections();
|
|
4452
|
+
var el = document.getElementById('backup-summary');
|
|
4453
|
+
if (!el) return;
|
|
4454
|
+
el.textContent = t('backup.restoring') + ': ' + sel.selections.length + ' ' + t('backup.sumApps') +
|
|
4455
|
+
', ' + sel.versionTotal + ' ' + t('backup.sumVersions') +
|
|
4456
|
+
(sel.extensions.length ? ', ' + sel.extensions.length + ' ' + t('backup.sumExts') : '');
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function backupSelectAll(checked) {
|
|
4460
|
+
var d = backupInspectData;
|
|
4461
|
+
if (!d) return;
|
|
4462
|
+
for (var i = 0; i < d.apps.length; i++) {
|
|
4463
|
+
var cb = document.getElementById('backup-app-' + i);
|
|
4464
|
+
if (cb) cb.checked = checked;
|
|
4465
|
+
document.querySelectorAll('.backup-ver[data-app="' + i + '"]').forEach(function (b) { b.checked = checked; });
|
|
4466
|
+
}
|
|
4467
|
+
for (var e = 0; e < d.extensions.length; e++) {
|
|
4468
|
+
var ecb = document.getElementById('backup-ext-' + e);
|
|
4469
|
+
if (ecb) ecb.checked = checked;
|
|
4470
|
+
}
|
|
4471
|
+
backupUpdateSummary();
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
function submitBackupRestore() {
|
|
4475
|
+
var sel = backupCollectSelections();
|
|
4476
|
+
var statusEl = document.getElementById('backup-status');
|
|
4477
|
+
if (sel.selections.length === 0 && sel.extensions.length === 0) {
|
|
4478
|
+
statusEl.style.color = '#ef4444';
|
|
4479
|
+
statusEl.textContent = t('backup.nothingSelected');
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
4483
|
+
statusEl.textContent = t('backup.restoring') + '...';
|
|
4484
|
+
var btn = document.getElementById('backup-restore-btn');
|
|
4485
|
+
btn.disabled = true;
|
|
4486
|
+
fetch(backupApiBase() + '/v1/apps/backup/restore', {
|
|
4487
|
+
method: 'POST',
|
|
4488
|
+
headers: {
|
|
4489
|
+
'Authorization': 'Bearer ' + getCortexOwnerToken(),
|
|
4490
|
+
'Content-Type': 'application/json'
|
|
4491
|
+
},
|
|
4492
|
+
body: JSON.stringify({
|
|
4493
|
+
backup_token: backupInspectData.backup_token,
|
|
4494
|
+
selections: sel.selections,
|
|
4495
|
+
extensions: sel.extensions
|
|
4496
|
+
})
|
|
4497
|
+
})
|
|
4498
|
+
.then(function (r) { return r.json(); })
|
|
4499
|
+
.then(function (json) {
|
|
4500
|
+
if (!json.ok) {
|
|
4501
|
+
var msg = (json.error && (json.error.message || json.error.code)) || 'Restore failed';
|
|
4502
|
+
throw new Error(msg);
|
|
4503
|
+
}
|
|
4504
|
+
statusEl.textContent = '';
|
|
4505
|
+
renderBackupResult(json.data);
|
|
4506
|
+
refreshAll();
|
|
4507
|
+
})
|
|
4508
|
+
.catch(function (e) {
|
|
4509
|
+
statusEl.style.color = '#ef4444';
|
|
4510
|
+
statusEl.textContent = e.message || String(e);
|
|
4511
|
+
})
|
|
4512
|
+
.finally(function () { btn.disabled = false; });
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
function renderBackupResult(s) {
|
|
4516
|
+
var body = document.getElementById('backup-import-body');
|
|
4517
|
+
document.getElementById('backup-restore-btn').style.display = 'none';
|
|
4518
|
+
var section = function (label, items) {
|
|
4519
|
+
if (!items || items.length === 0) return '';
|
|
4520
|
+
var rendered = items.map(function (x) {
|
|
4521
|
+
if (typeof x === 'string') return escapeHtml(x);
|
|
4522
|
+
if (x && x.from && x.to) return escapeHtml(x.from) + ' → ' + escapeHtml(x.to);
|
|
4523
|
+
if (x && x.item) return escapeHtml(x.item) + ': ' + escapeHtml(x.message || '');
|
|
4524
|
+
return escapeHtml(JSON.stringify(x));
|
|
4525
|
+
});
|
|
4526
|
+
return '<div style="font-size:.85rem;font-weight:600;margin-top:8px">' + label + ' (' + items.length + ')</div>' +
|
|
4527
|
+
'<ul class="backup-result-list">' + rendered.map(function (r) { return '<li>' + r + '</li>'; }).join('') + '</ul>';
|
|
4528
|
+
};
|
|
4529
|
+
body.innerHTML =
|
|
4530
|
+
'<h3 style="font-size:.95rem;margin:6px 0">✔ ' + t('backup.resultTitle') + '</h3>' +
|
|
4531
|
+
'<div style="font-size:.85rem">' + t('backup.resVersions') + ': ' + s.versions_restored + '</div>' +
|
|
4532
|
+
section(t('backup.resCreated'), s.apps_created.concat(s.extensions_created)) +
|
|
4533
|
+
section(t('backup.resAppended'), s.apps_appended) +
|
|
4534
|
+
section(t('backup.resCopied'), s.apps_copied.concat(s.extensions_copied)) +
|
|
4535
|
+
section(t('backup.resSkipped'), s.apps_skipped.concat(s.extensions_skipped)) +
|
|
4536
|
+
(s.errors.length ? '<div class="backup-result-errors">' + section(t('backup.resErrors'), s.errors) + '</div>' : '');
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
// ── Server → local import (user-selected, NEVER automatic) ────
|
|
4540
|
+
// Apps published under this owner's account (e.g. via MCP) may exist only on
|
|
4541
|
+
// the server. Importing one into the local catalog gives it the exact same
|
|
4542
|
+
// management actions (edit, rename, tags, versions, delete) as locally
|
|
4543
|
+
// created apps. The import is ALWAYS user-initiated: a banner reports how
|
|
4544
|
+
// many published apps are missing locally, and a review modal lets the user
|
|
4545
|
+
// pick exactly which ones to import. The tombstone list marks apps the user
|
|
4546
|
+
// deleted locally so the banner doesn't nag about them (they stay reachable
|
|
4547
|
+
// in the modal, unchecked).
|
|
4548
|
+
|
|
4549
|
+
var IMPORT_IGNORE_KEY = 'appCatalogImportIgnore';
|
|
4550
|
+
|
|
4551
|
+
function getImportIgnore() {
|
|
4552
|
+
try { return JSON.parse(localStorage.getItem(IMPORT_IGNORE_KEY) || '[]'); } catch (e) { return []; }
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
function addImportIgnore(filename) {
|
|
4556
|
+
var list = getImportIgnore();
|
|
4557
|
+
if (list.indexOf(filename) === -1) {
|
|
4558
|
+
list.push(filename);
|
|
4559
|
+
localStorage.setItem(IMPORT_IGNORE_KEY, JSON.stringify(list));
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
function removeImportIgnore(filename) {
|
|
4564
|
+
var list = getImportIgnore().filter(function (f) { return f !== filename; });
|
|
4565
|
+
localStorage.setItem(IMPORT_IGNORE_KEY, JSON.stringify(list));
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
// State for the review modal: own server apps with no local copy.
|
|
4569
|
+
var serverImportState = { missing: [], aimeatUrl: '' };
|
|
4570
|
+
|
|
4571
|
+
function updateServerImportState(ownApps, localByFilename, aimeatUrl) {
|
|
4572
|
+
serverImportState.missing = ownApps.filter(function (sa) {
|
|
4573
|
+
return sa.filename && !localByFilename[sa.filename];
|
|
4574
|
+
});
|
|
4575
|
+
serverImportState.aimeatUrl = aimeatUrl;
|
|
4576
|
+
renderServerImportBanner();
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
function renderServerImportBanner() {
|
|
4580
|
+
var banner = document.getElementById('server-import-banner');
|
|
4581
|
+
if (!banner) return;
|
|
4582
|
+
var ignore = getImportIgnore();
|
|
4583
|
+
// Tombstoned apps (deleted locally on purpose) don't count toward the nag
|
|
4584
|
+
var fresh = serverImportState.missing.filter(function (sa) { return ignore.indexOf(sa.filename) === -1; });
|
|
4585
|
+
if (fresh.length === 0 || sessionStorage.getItem('aimeatServerImportDismissed') === '1') {
|
|
4586
|
+
banner.style.display = 'none';
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
document.getElementById('server-import-count').textContent = fresh.length;
|
|
4590
|
+
banner.style.display = '';
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
function dismissServerImportBanner() {
|
|
4594
|
+
sessionStorage.setItem('aimeatServerImportDismissed', '1');
|
|
4595
|
+
renderServerImportBanner();
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
function showServerImportModal() {
|
|
4599
|
+
var missing = serverImportState.missing;
|
|
4600
|
+
var body = document.getElementById('server-import-body');
|
|
4601
|
+
var ignore = getImportIgnore();
|
|
4602
|
+
var html = '<p style="font-size:.8rem;color:var(--text-muted);margin:4px 0 8px">' + t('srvImport.desc') + '</p>' +
|
|
4603
|
+
'<div class="backup-toolbar-row"><div>' +
|
|
4604
|
+
'<button class="backup-link-btn" onclick="window._launcher.serverImportSelectAll(true)">' + t('backup.selectAll') + '</button> / ' +
|
|
4605
|
+
'<button class="backup-link-btn" onclick="window._launcher.serverImportSelectAll(false)">' + t('backup.selectNone') + '</button>' +
|
|
4606
|
+
'</div></div>' +
|
|
4607
|
+
'<table class="backup-table"><tbody>';
|
|
4608
|
+
for (var i = 0; i < missing.length; i++) {
|
|
4609
|
+
var sa = missing[i];
|
|
4610
|
+
var name = (sa.manifest && sa.manifest.name) ? sa.manifest.name : sa.filename;
|
|
4611
|
+
// Locally deleted (tombstoned) apps stay reachable here but default to unchecked
|
|
4612
|
+
var checked = ignore.indexOf(sa.filename) === -1 ? ' checked' : '';
|
|
4613
|
+
html +=
|
|
4614
|
+
'<tr>' +
|
|
4615
|
+
'<td><input type="checkbox" class="server-import-cb" data-i="' + i + '"' + checked + '/></td>' +
|
|
4616
|
+
'<td>' + escapeHtml(name) + '<div class="mono-cell">' + escapeHtml(sa.filename) + '</div></td>' +
|
|
4617
|
+
'<td>' + (sa.version_number ? 'v' + sa.version_number : '') + '</td>' +
|
|
4618
|
+
'<td>' + (sa.created_at ? new Date(sa.created_at).toLocaleDateString() : '') + '</td>' +
|
|
4619
|
+
'</tr>';
|
|
4620
|
+
}
|
|
4621
|
+
html += '</tbody></table>';
|
|
4622
|
+
body.innerHTML = html;
|
|
4623
|
+
document.getElementById('server-import-status').textContent = '';
|
|
4624
|
+
document.getElementById('server-import-overlay').hidden = false;
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
function serverImportSelectAll(checked) {
|
|
4628
|
+
document.querySelectorAll('.server-import-cb').forEach(function (b) { b.checked = checked; });
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
function submitServerImport() {
|
|
4632
|
+
var selected = [];
|
|
4633
|
+
document.querySelectorAll('.server-import-cb').forEach(function (b) {
|
|
4634
|
+
if (b.checked) selected.push(serverImportState.missing[parseInt(b.getAttribute('data-i'), 10)]);
|
|
4635
|
+
});
|
|
4636
|
+
var statusEl = document.getElementById('server-import-status');
|
|
4637
|
+
if (selected.length === 0) {
|
|
4638
|
+
statusEl.style.color = '#ef4444';
|
|
4639
|
+
statusEl.textContent = t('backup.nothingSelected');
|
|
4640
|
+
return;
|
|
4641
|
+
}
|
|
4642
|
+
statusEl.style.color = 'var(--text-muted)';
|
|
4643
|
+
statusEl.textContent = t('srvImport.importing');
|
|
4644
|
+
importServerApps(selected, serverImportState.aimeatUrl).then(function () {
|
|
4645
|
+
// Importing an app the user once deleted is an explicit re-opt-in
|
|
4646
|
+
selected.forEach(function (sa) { removeImportIgnore(sa.filename); });
|
|
4647
|
+
document.getElementById('server-import-overlay').hidden = true;
|
|
4648
|
+
renderApps();
|
|
4649
|
+
loadPublishedApps();
|
|
4650
|
+
}).catch(function (e) {
|
|
4651
|
+
statusEl.style.color = '#ef4444';
|
|
4652
|
+
statusEl.textContent = e.message || String(e);
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
function importServerApps(toImport, aimeatUrl) {
|
|
4657
|
+
if (toImport.length === 0) return Promise.resolve(false);
|
|
4658
|
+
return Promise.all(toImport.map(function (sa) {
|
|
4659
|
+
var url = aimeatUrl + '/v1/apps/' + encodeURIComponent(sa.owner || '') + '/' + encodeURIComponent(sa.filename) + '?mode=inline';
|
|
4660
|
+
return fetch(url)
|
|
4661
|
+
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
|
|
4662
|
+
.then(function (source) {
|
|
4663
|
+
var manifest = sa.manifest || {};
|
|
4664
|
+
var app = {
|
|
4665
|
+
id: generateId(),
|
|
4666
|
+
name: manifest.name || sa.filename,
|
|
4667
|
+
description: manifest.description || '',
|
|
4668
|
+
source: 'aimeat',
|
|
4669
|
+
origin: 'ai-published',
|
|
4670
|
+
url: null,
|
|
4671
|
+
blob: btoa(unescape(encodeURIComponent(source))),
|
|
4672
|
+
tags: manifest.tags || [],
|
|
4673
|
+
openMode: 'tab',
|
|
4674
|
+
icon: manifest.icon || '\u{1F916}',
|
|
4675
|
+
screenshot: null,
|
|
4676
|
+
favorite: false,
|
|
4677
|
+
addedAt: sa.created_at || new Date().toISOString(),
|
|
4678
|
+
lastOpenedAt: null,
|
|
4679
|
+
published: true,
|
|
4680
|
+
publishedFilename: sa.filename,
|
|
4681
|
+
publishedAt: sa.created_at || new Date().toISOString(),
|
|
4682
|
+
publishedUrl: '/v1/apps/' + encodeURIComponent(sa.owner || '') + '/' + encodeURIComponent(sa.filename),
|
|
4683
|
+
publishedVersionNumber: sa.version_number || 1
|
|
4684
|
+
};
|
|
4685
|
+
return saveApp(app);
|
|
4686
|
+
})
|
|
4687
|
+
.catch(function () { return null; }); // protected/unreachable app — skip silently
|
|
4688
|
+
})).then(function () { return true; });
|
|
4689
|
+
}
|
|
4690
|
+
|
|
3666
4691
|
// Manifest cache keyed by "owner\nfilename" — lets Restore/Fork reuse the
|
|
3667
4692
|
// app's metadata (name, description, category, tags, icon) without a re-fetch.
|
|
3668
4693
|
var serverAppManifests = {};
|
|
@@ -3729,8 +4754,14 @@
|
|
|
3729
4754
|
? serverApps.filter(function(a) { return !sameOwner(a.owner, currentOwner); })
|
|
3730
4755
|
: serverApps;
|
|
3731
4756
|
|
|
3732
|
-
|
|
3733
|
-
|
|
4757
|
+
return loadSubdomainSites().then(function () {
|
|
4758
|
+
renderPublishedApps(localPublished, ownApps, localByFilename, aimeatUrl, section, grid, countEl);
|
|
4759
|
+
renderCommunityApps(communityApps, aimeatUrl, communitySection, communityGrid, communityCountEl, currentOwner);
|
|
4760
|
+
// Server-published own apps missing from the local catalog are
|
|
4761
|
+
// NEVER imported automatically — the banner offers a review modal
|
|
4762
|
+
// where the user picks exactly which ones to import.
|
|
4763
|
+
updateServerImportState(ownApps, localByFilename, aimeatUrl);
|
|
4764
|
+
});
|
|
3734
4765
|
})
|
|
3735
4766
|
.catch(function() {
|
|
3736
4767
|
renderPublishedApps(localPublished, [], localByFilename, aimeatUrl, section, grid, countEl);
|
|
@@ -3829,11 +4860,29 @@
|
|
|
3829
4860
|
section.style.display = '';
|
|
3830
4861
|
countEl.textContent = '(' + merged.length + ')';
|
|
3831
4862
|
|
|
4863
|
+
// Operator-only: subdomain chip/assign controls on own server-published cards
|
|
4864
|
+
var showSubdomainControls = !!subdomainsByTarget && isOperatorSession();
|
|
4865
|
+
|
|
3832
4866
|
var html = '';
|
|
3833
4867
|
for (var i = 0; i < merged.length; i++) {
|
|
3834
4868
|
var app = merged[i];
|
|
3835
4869
|
var badgeClass = app.source === 'both' ? 'both' : (app.source === 'server' ? 'server' : 'local');
|
|
3836
4870
|
var badgeLabel = app.source === 'both' ? 'PORTAL + MCP' : (app.source === 'server' ? 'MCP' : 'PORTAL');
|
|
4871
|
+
|
|
4872
|
+
var subChipHtml = '';
|
|
4873
|
+
var subBtnHtml = '';
|
|
4874
|
+
if (showSubdomainControls && app.isOwn && app.filename) {
|
|
4875
|
+
var targetKey = bareOwnerName(app.owner) + '/' + app.filename;
|
|
4876
|
+
var mapping = subdomainsByTarget[targetKey] || null;
|
|
4877
|
+
var openModal = 'window._launcher.showSubdomainModal(\'' + escapeHtml(app.owner || '') + '\', \'' + escapeHtml(app.filename) + '\')';
|
|
4878
|
+
if (mapping) {
|
|
4879
|
+
subChipHtml = '<div><span class="subdomain-chip" onclick="' + openModal + '" title="' + t('subModal.title') + '">🌐 ' +
|
|
4880
|
+
escapeHtml(mapping.subdomain + '.' + apexHostLabel()) + '</span></div>';
|
|
4881
|
+
} else {
|
|
4882
|
+
subBtnHtml = '<button onclick="' + openModal + '">' + t('card.subdomain') + '</button>';
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
|
|
3837
4886
|
html +=
|
|
3838
4887
|
'<div class="published-card">' +
|
|
3839
4888
|
'<div class="published-card-name">' + escapeHtml(app.name) + (app.version ? ' <span style="opacity:.5;font-size:.85em">' + escapeHtml(app.version) + '</span>' : '') +
|
|
@@ -3843,9 +4892,11 @@
|
|
|
3843
4892
|
'<span>' + escapeHtml(app.filename) + '</span>' +
|
|
3844
4893
|
'<span>' + app.date + '</span>' +
|
|
3845
4894
|
'</div>' +
|
|
4895
|
+
subChipHtml +
|
|
3846
4896
|
'<div class="published-card-actions">' +
|
|
3847
4897
|
(app.viewUrl ? '<button onclick="window.open(\'' + escapeHtml(app.viewUrl) + '?mode=inline\', \'_blank\')">' + t('card.view') + '</button>' : '') +
|
|
3848
4898
|
(app.isOwn && app.filename ? '<button onclick="window._launcher.showVersionsModal(\'' + escapeHtml(app.owner || '') + '\', \'' + escapeHtml(app.filename) + '\')">' + t('card.versions') + '</button>' : '') +
|
|
4899
|
+
subBtnHtml +
|
|
3849
4900
|
(app.localId ? '<button onclick="window._launcher.unpublishApp(\'' + escapeHtml(app.localId) + '\')" class="danger">' + t('card.remove') + '</button>' :
|
|
3850
4901
|
app.isOwn ? '<button onclick="window._launcher.deleteServerApp(\'' + escapeHtml(app.filename) + '\')" class="danger">' + t('card.remove') + '</button>' : '') +
|
|
3851
4902
|
'</div>' +
|
|
@@ -3875,6 +4926,7 @@
|
|
|
3875
4926
|
.then(function(resp) { return resp.json(); })
|
|
3876
4927
|
.then(function(json) {
|
|
3877
4928
|
if (json.ok) {
|
|
4929
|
+
removeImportIgnore(app.publishedFilename);
|
|
3878
4930
|
app.published = false;
|
|
3879
4931
|
delete app.publishedFilename;
|
|
3880
4932
|
delete app.publishedAt;
|
|
@@ -3906,6 +4958,7 @@
|
|
|
3906
4958
|
.then(function(resp) { return resp.json(); })
|
|
3907
4959
|
.then(function(json) {
|
|
3908
4960
|
if (json.ok) {
|
|
4961
|
+
removeImportIgnore(filename); // a future republish should import again
|
|
3909
4962
|
loadPublishedApps();
|
|
3910
4963
|
} else {
|
|
3911
4964
|
alert('Failed to delete: ' + (json.error && json.error.message ? json.error.message : 'Unknown error'));
|
|
@@ -4886,6 +5939,22 @@
|
|
|
4886
5939
|
switchDbMode: switchDbMode,
|
|
4887
5940
|
unpublishApp: unpublishApp,
|
|
4888
5941
|
deleteServerApp: deleteServerApp,
|
|
5942
|
+
showSubdomainModal: showSubdomainModal,
|
|
5943
|
+
submitSubdomainAssign: submitSubdomainAssign,
|
|
5944
|
+
unassignSubdomain: unassignSubdomain,
|
|
5945
|
+
jsonImportSelectAll: jsonImportSelectAll,
|
|
5946
|
+
submitJsonImport: submitJsonImport,
|
|
5947
|
+
removeDuplicateApps: removeDuplicateApps,
|
|
5948
|
+
showServerImportModal: showServerImportModal,
|
|
5949
|
+
dismissServerImportBanner: dismissServerImportBanner,
|
|
5950
|
+
serverImportSelectAll: serverImportSelectAll,
|
|
5951
|
+
submitServerImport: submitServerImport,
|
|
5952
|
+
toggleBackupMenu: toggleBackupMenu,
|
|
5953
|
+
exportBackupZip: exportBackupZip,
|
|
5954
|
+
importBackupPick: importBackupPick,
|
|
5955
|
+
backupSelectAll: backupSelectAll,
|
|
5956
|
+
backupUpdateSummary: backupUpdateSummary,
|
|
5957
|
+
submitBackupRestore: submitBackupRestore,
|
|
4889
5958
|
showVersionsModal: showVersionsModal,
|
|
4890
5959
|
restoreVersion: restoreVersion,
|
|
4891
5960
|
forkVersion: forkVersion,
|
|
@@ -5002,6 +6071,40 @@
|
|
|
5002
6071
|
}
|
|
5003
6072
|
});
|
|
5004
6073
|
|
|
6074
|
+
// ── Click subdomain overlay to close ────────────
|
|
6075
|
+
document.getElementById('subdomain-overlay').addEventListener('click', function (e) {
|
|
6076
|
+
if (e.target === this) {
|
|
6077
|
+
this.hidden = true;
|
|
6078
|
+
}
|
|
6079
|
+
});
|
|
6080
|
+
|
|
6081
|
+
// ── Backup: file picker + overlay/menu close ────
|
|
6082
|
+
document.getElementById('backup-file-input').addEventListener('change', function () {
|
|
6083
|
+
if (this.files && this.files[0]) importBackupFile(this.files[0]);
|
|
6084
|
+
this.value = '';
|
|
6085
|
+
});
|
|
6086
|
+
document.getElementById('backup-overlay').addEventListener('click', function (e) {
|
|
6087
|
+
if (e.target === this) {
|
|
6088
|
+
this.hidden = true;
|
|
6089
|
+
}
|
|
6090
|
+
});
|
|
6091
|
+
document.getElementById('server-import-overlay').addEventListener('click', function (e) {
|
|
6092
|
+
if (e.target === this) {
|
|
6093
|
+
this.hidden = true;
|
|
6094
|
+
}
|
|
6095
|
+
});
|
|
6096
|
+
document.getElementById('json-import-overlay').addEventListener('click', function (e) {
|
|
6097
|
+
if (e.target === this) {
|
|
6098
|
+
this.hidden = true;
|
|
6099
|
+
}
|
|
6100
|
+
});
|
|
6101
|
+
document.addEventListener('click', function (e) {
|
|
6102
|
+
var menu = document.getElementById('backup-menu');
|
|
6103
|
+
if (!menu.hidden && !menu.contains(e.target) && e.target.id !== 'backup-btn') {
|
|
6104
|
+
menu.hidden = true;
|
|
6105
|
+
}
|
|
6106
|
+
});
|
|
6107
|
+
|
|
5005
6108
|
// ── Click cortex popup overlay to close ─────────
|
|
5006
6109
|
document.getElementById('cortex-popup-overlay').addEventListener('click', function (e) {
|
|
5007
6110
|
if (e.target === this) {
|
|
@@ -5142,6 +6245,14 @@
|
|
|
5142
6245
|
document.getElementById('aimeat-import-overlay').hidden = true;
|
|
5143
6246
|
} else if (!document.getElementById('publish-overlay').hidden) {
|
|
5144
6247
|
document.getElementById('publish-overlay').hidden = true;
|
|
6248
|
+
} else if (!document.getElementById('subdomain-overlay').hidden) {
|
|
6249
|
+
document.getElementById('subdomain-overlay').hidden = true;
|
|
6250
|
+
} else if (!document.getElementById('backup-overlay').hidden) {
|
|
6251
|
+
document.getElementById('backup-overlay').hidden = true;
|
|
6252
|
+
} else if (!document.getElementById('server-import-overlay').hidden) {
|
|
6253
|
+
document.getElementById('server-import-overlay').hidden = true;
|
|
6254
|
+
} else if (!document.getElementById('json-import-overlay').hidden) {
|
|
6255
|
+
document.getElementById('json-import-overlay').hidden = true;
|
|
5145
6256
|
} else if (!document.getElementById('settings-overlay').hidden) {
|
|
5146
6257
|
closeSettings();
|
|
5147
6258
|
} else if (!document.getElementById('modal-overlay').hidden) {
|