@kaikybrofc/omnizap-system 2.2.3 → 2.2.5
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/.env.example +13 -0
- package/README.md +29 -85
- package/app/controllers/messageController.js +133 -1
- package/app/modules/stickerPackModule/catalogHandlers/catalogAdminHttp.js +68 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogAuthHttp.js +34 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogPublicHttp.js +179 -0
- package/app/modules/stickerPackModule/catalogHandlers/catalogUploadHttp.js +92 -0
- package/app/modules/stickerPackModule/catalogRouter.js +79 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +243 -0
- package/app/modules/stickerPackModule/domainEvents.js +61 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +21 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +19 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +55 -15
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +238 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +71 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +198 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +1090 -659
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +19 -1
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +44 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +18 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +51 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +191 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +301 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +111 -10
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +21 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +59 -7
- package/app/observability/metrics.js +169 -0
- package/app/services/featureFlagService.js +137 -0
- package/app/services/lidMapService.js +4 -1
- package/app/services/whatsappLoginLinkService.js +232 -0
- package/database/index.js +5 -0
- package/database/migrations/20260228_0021_sticker_web_google_owner_phone.sql +83 -0
- package/database/migrations/20260228_0022_sticker_scale_indexes.sql +16 -0
- package/database/migrations/20260228_0023_sticker_pack_score_snapshot.sql +25 -0
- package/database/migrations/20260228_0024_domain_event_outbox.sql +42 -0
- package/database/migrations/20260228_0025_sticker_worker_task_idempotency_dlq.sql +23 -0
- package/database/migrations/20260228_0026_feature_flags.sql +21 -0
- package/ecosystem.prod.config.cjs +70 -9
- package/index.js +26 -0
- package/package.json +5 -1
- package/public/index.html +128 -10
- package/public/js/apps/createPackApp.js +59 -272
- package/public/js/apps/homeApp.js +106 -0
- package/public/js/apps/loginApp.js +459 -0
- package/public/js/apps/stickersApp.js +34 -37
- package/public/js/apps/userApp.js +244 -0
- package/public/js/runtime/react-runtime.js +1 -0
- package/public/login/index.html +333 -0
- package/public/stickers/create/index.html +2 -1
- package/public/stickers/index.html +2 -1
- package/public/user/index.html +367 -0
- package/scripts/cache-bust.mjs +65 -11
- package/scripts/sticker-catalog-loadtest.mjs +208 -0
- package/scripts/sticker-worker-task.mjs +122 -0
|
@@ -2,6 +2,21 @@ require('dotenv').config();
|
|
|
2
2
|
|
|
3
3
|
const appName = process.env.PM2_APP_NAME || 'omnizap-system';
|
|
4
4
|
|
|
5
|
+
const baseEnv = {
|
|
6
|
+
NODE_ENV: 'production',
|
|
7
|
+
COMMAND_PREFIX: '/',
|
|
8
|
+
LOG_LEVEL: 'info',
|
|
9
|
+
DB_LOG_EVERY_QUERY: 'false',
|
|
10
|
+
DB_MONITOR_ENABLED: 'false',
|
|
11
|
+
LID_BACKFILL_ON_START: 'false',
|
|
12
|
+
STICKER_CLASSIFICATION_BACKGROUND_ENABLED: 'true',
|
|
13
|
+
STICKER_REPROCESS_QUEUE_ENABLED: 'true',
|
|
14
|
+
STICKER_AUTO_PACK_BY_TAGS_ENABLED: 'true',
|
|
15
|
+
STICKER_WORKER_PIPELINE_ENABLED: 'true',
|
|
16
|
+
STICKER_WORKER_PIPELINE_INLINE_POLLER_ENABLED: 'true',
|
|
17
|
+
STICKER_DEDICATED_WORKERS_ENABLED: 'true',
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
module.exports = {
|
|
6
21
|
apps: [
|
|
7
22
|
{
|
|
@@ -17,19 +32,65 @@ module.exports = {
|
|
|
17
32
|
out_file: `logs/${appName}-out.log`,
|
|
18
33
|
error_file: `logs/${appName}-error.log`,
|
|
19
34
|
env: {
|
|
20
|
-
|
|
21
|
-
COMMAND_PREFIX: '/',
|
|
22
|
-
LOG_LEVEL: 'info',
|
|
23
|
-
DB_LOG_EVERY_QUERY: 'false',
|
|
24
|
-
DB_MONITOR_ENABLED: 'false',
|
|
25
|
-
LID_BACKFILL_ON_START: 'false',
|
|
26
|
-
STICKER_CLASSIFICATION_BACKGROUND_ENABLED: 'true',
|
|
27
|
-
STICKER_REPROCESS_QUEUE_ENABLED: 'true',
|
|
28
|
-
STICKER_AUTO_PACK_BY_TAGS_ENABLED: 'true',
|
|
35
|
+
...baseEnv,
|
|
29
36
|
},
|
|
30
37
|
wait_ready: true,
|
|
31
38
|
listen_timeout: 10000,
|
|
32
39
|
kill_timeout: 5000,
|
|
33
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: `${appName}-worker-classification`,
|
|
43
|
+
script: './scripts/sticker-worker-task.mjs',
|
|
44
|
+
args: '--task-type classification_cycle',
|
|
45
|
+
cwd: __dirname,
|
|
46
|
+
exec_mode: 'fork',
|
|
47
|
+
instances: 1,
|
|
48
|
+
autorestart: true,
|
|
49
|
+
watch: false,
|
|
50
|
+
max_memory_restart: '2G',
|
|
51
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
52
|
+
out_file: `logs/${appName}-worker-classification-out.log`,
|
|
53
|
+
error_file: `logs/${appName}-worker-classification-error.log`,
|
|
54
|
+
env: {
|
|
55
|
+
...baseEnv,
|
|
56
|
+
},
|
|
57
|
+
kill_timeout: 5000,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: `${appName}-worker-curation`,
|
|
61
|
+
script: './scripts/sticker-worker-task.mjs',
|
|
62
|
+
args: '--task-type curation_cycle',
|
|
63
|
+
cwd: __dirname,
|
|
64
|
+
exec_mode: 'fork',
|
|
65
|
+
instances: 1,
|
|
66
|
+
autorestart: true,
|
|
67
|
+
watch: false,
|
|
68
|
+
max_memory_restart: '2G',
|
|
69
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
70
|
+
out_file: `logs/${appName}-worker-curation-out.log`,
|
|
71
|
+
error_file: `logs/${appName}-worker-curation-error.log`,
|
|
72
|
+
env: {
|
|
73
|
+
...baseEnv,
|
|
74
|
+
},
|
|
75
|
+
kill_timeout: 5000,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: `${appName}-worker-rebuild`,
|
|
79
|
+
script: './scripts/sticker-worker-task.mjs',
|
|
80
|
+
args: '--task-type rebuild_cycle',
|
|
81
|
+
cwd: __dirname,
|
|
82
|
+
exec_mode: 'fork',
|
|
83
|
+
instances: 1,
|
|
84
|
+
autorestart: true,
|
|
85
|
+
watch: false,
|
|
86
|
+
max_memory_restart: '2G',
|
|
87
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
88
|
+
out_file: `logs/${appName}-worker-rebuild-out.log`,
|
|
89
|
+
error_file: `logs/${appName}-worker-rebuild-error.log`,
|
|
90
|
+
env: {
|
|
91
|
+
...baseEnv,
|
|
92
|
+
},
|
|
93
|
+
kill_timeout: 5000,
|
|
94
|
+
},
|
|
34
95
|
],
|
|
35
96
|
};
|
package/index.js
CHANGED
|
@@ -39,6 +39,14 @@ import {
|
|
|
39
39
|
startStickerWorkerPipeline,
|
|
40
40
|
stopStickerWorkerPipeline,
|
|
41
41
|
} from './app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js';
|
|
42
|
+
import {
|
|
43
|
+
startStickerPackScoreSnapshotRuntime,
|
|
44
|
+
stopStickerPackScoreSnapshotRuntime,
|
|
45
|
+
} from './app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js';
|
|
46
|
+
import {
|
|
47
|
+
startStickerDomainEventConsumer,
|
|
48
|
+
stopStickerDomainEventConsumer,
|
|
49
|
+
} from './app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js';
|
|
42
50
|
|
|
43
51
|
/**
|
|
44
52
|
* Timeout máximo para inicialização do banco (criar/verificar DB + tabelas).
|
|
@@ -216,6 +224,8 @@ async function startApp() {
|
|
|
216
224
|
startStickerClassificationBackground();
|
|
217
225
|
startStickerAutoPackByTagsBackground();
|
|
218
226
|
}
|
|
227
|
+
startStickerPackScoreSnapshotRuntime();
|
|
228
|
+
startStickerDomainEventConsumer();
|
|
219
229
|
|
|
220
230
|
// Backfill é opcional, rodando em background.
|
|
221
231
|
const shouldBackfill = process.env.LID_BACKFILL_ON_START !== 'false';
|
|
@@ -363,6 +373,22 @@ async function shutdown(signal, error) {
|
|
|
363
373
|
});
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
try {
|
|
377
|
+
stopStickerPackScoreSnapshotRuntime();
|
|
378
|
+
} catch (snapshotError) {
|
|
379
|
+
logger.warn('Falha ao encerrar runtime de snapshot de score dos packs.', {
|
|
380
|
+
error: snapshotError?.message,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
stopStickerDomainEventConsumer();
|
|
386
|
+
} catch (consumerError) {
|
|
387
|
+
logger.warn('Falha ao encerrar consumidor interno de eventos de domínio.', {
|
|
388
|
+
error: consumerError?.message,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
366
392
|
// 5) Encerrar MySQL pool
|
|
367
393
|
await closeDatabasePool();
|
|
368
394
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaikybrofc/omnizap-system",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"description": "Sistema profissional de automação WhatsApp com tecnologia Baileys",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"deploy": "bash ./scripts/deploy.sh",
|
|
60
60
|
"deploy:dry-run": "DEPLOY_DRY_RUN=1 bash ./scripts/deploy.sh",
|
|
61
61
|
"readme:sync-snapshot": "node ./scripts/sync-readme-snapshot.mjs",
|
|
62
|
+
"loadtest:stickers": "node ./scripts/sticker-catalog-loadtest.mjs",
|
|
63
|
+
"worker:sticker:classification": "node ./scripts/sticker-worker-task.mjs --task-type classification_cycle",
|
|
64
|
+
"worker:sticker:curation": "node ./scripts/sticker-worker-task.mjs --task-type curation_cycle",
|
|
65
|
+
"worker:sticker:rebuild": "node ./scripts/sticker-worker-task.mjs --task-type rebuild_cycle",
|
|
62
66
|
"release": "bash ./scripts/release.sh",
|
|
63
67
|
"release:minor": "RELEASE_TYPE=minor bash ./scripts/release.sh",
|
|
64
68
|
"release:major": "RELEASE_TYPE=major bash ./scripts/release.sh",
|
package/public/index.html
CHANGED
|
@@ -138,8 +138,63 @@
|
|
|
138
138
|
|
|
139
139
|
.nav { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
.nav-user-chip {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
padding: 6px 10px 6px 6px;
|
|
146
|
+
max-width: 270px;
|
|
147
|
+
border-color: #3f5f8f;
|
|
148
|
+
background: linear-gradient(120deg, #132544d6, #10203ad6);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.nav-user-avatar-bubble {
|
|
152
|
+
display: inline-flex;
|
|
153
|
+
width: 30px;
|
|
154
|
+
height: 30px;
|
|
155
|
+
border-radius: 999px;
|
|
156
|
+
border: 1px solid #4a6fa4;
|
|
157
|
+
background: #132340;
|
|
158
|
+
box-shadow: 0 0 0 2px #1a2e4b, 0 0 16px #6aaaf236;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
flex: 0 0 auto;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nav-user-photo {
|
|
164
|
+
width: 100%;
|
|
165
|
+
height: 100%;
|
|
166
|
+
object-fit: cover;
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.nav-user-name-bubble {
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 6px;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
padding: 4px 8px;
|
|
176
|
+
border-radius: 999px;
|
|
177
|
+
border: 1px solid #34547f;
|
|
178
|
+
background: #12213ac9;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.nav-user-icon {
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
color: #9bc6f5;
|
|
184
|
+
flex: 0 0 auto;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.nav-user-name {
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
font-weight: 700;
|
|
190
|
+
color: #dbe8fb;
|
|
191
|
+
white-space: nowrap;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
text-overflow: ellipsis;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.nav-toggle {
|
|
197
|
+
display: none;
|
|
143
198
|
width: 42px;
|
|
144
199
|
height: 42px;
|
|
145
200
|
border-radius: 10px;
|
|
@@ -178,12 +233,12 @@
|
|
|
178
233
|
|
|
179
234
|
.btn:hover { transform: translateY(-1px); border-color: #3f567f; }
|
|
180
235
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
236
|
+
.btn.primary {
|
|
237
|
+
color: #042511;
|
|
238
|
+
border-color: transparent;
|
|
239
|
+
background: linear-gradient(90deg, var(--primary), #16a34a);
|
|
240
|
+
box-shadow: 0 8px 22px #22c55e33;
|
|
241
|
+
}
|
|
187
242
|
|
|
188
243
|
@media (max-width: 860px) {
|
|
189
244
|
.top-inner {
|
|
@@ -221,6 +276,33 @@
|
|
|
221
276
|
display: grid;
|
|
222
277
|
}
|
|
223
278
|
|
|
279
|
+
body.home-authenticated .top-inner {
|
|
280
|
+
flex-direction: row;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
gap: 10px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
body.home-authenticated .top-head {
|
|
287
|
+
flex: 1 1 auto;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
body.home-authenticated #nav-toggle {
|
|
291
|
+
display: none;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
body.home-authenticated #main-nav {
|
|
295
|
+
display: flex;
|
|
296
|
+
width: auto;
|
|
297
|
+
grid-template-columns: none;
|
|
298
|
+
gap: 8px;
|
|
299
|
+
flex-wrap: nowrap;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
body.home-authenticated #main-nav .btn:not(#nav-auth-link) {
|
|
303
|
+
display: none;
|
|
304
|
+
}
|
|
305
|
+
|
|
224
306
|
.btn {
|
|
225
307
|
width: 100%;
|
|
226
308
|
text-align: center;
|
|
@@ -228,6 +310,38 @@
|
|
|
228
310
|
border-radius: 10px;
|
|
229
311
|
}
|
|
230
312
|
|
|
313
|
+
.nav-user-chip {
|
|
314
|
+
justify-content: flex-start;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
body.home-authenticated #main-nav #nav-auth-link.nav-user-chip {
|
|
318
|
+
width: 44px;
|
|
319
|
+
min-width: 44px;
|
|
320
|
+
max-width: 44px;
|
|
321
|
+
height: 44px;
|
|
322
|
+
padding: 4px;
|
|
323
|
+
border-radius: 999px;
|
|
324
|
+
justify-content: center;
|
|
325
|
+
justify-self: auto;
|
|
326
|
+
align-self: auto;
|
|
327
|
+
background: linear-gradient(120deg, #132544f0, #10203af0);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
body.home-authenticated #main-nav #nav-auth-link.nav-user-chip .nav-user-name-bubble {
|
|
331
|
+
display: none;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
body.home-authenticated #main-nav #nav-auth-link.nav-user-chip .nav-user-avatar-bubble {
|
|
335
|
+
width: 34px;
|
|
336
|
+
height: 34px;
|
|
337
|
+
border: 1px solid #4a6fa4;
|
|
338
|
+
box-shadow: 0 0 0 1px #1a2e4b, 0 0 12px #6aaaf236;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
body.home-authenticated #main-nav #nav-scheduler-link {
|
|
342
|
+
display: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
231
345
|
}
|
|
232
346
|
|
|
233
347
|
@media (max-width: 420px) {
|
|
@@ -1057,7 +1171,8 @@
|
|
|
1057
1171
|
>☰</button>
|
|
1058
1172
|
</div>
|
|
1059
1173
|
<nav id="main-nav" class="nav">
|
|
1060
|
-
<a class="btn" href="/api-docs/"><i class="fa-solid fa-code icon-inline" aria-hidden="true"></i>API</a>
|
|
1174
|
+
<a id="nav-scheduler-link" class="btn" href="/api-docs/"><i class="fa-solid fa-code icon-inline" aria-hidden="true"></i>API</a>
|
|
1175
|
+
<a id="nav-auth-link" class="btn" href="/login/"><i class="fa-solid fa-right-to-bracket icon-inline" aria-hidden="true"></i>Login</a>
|
|
1061
1176
|
<a class="btn" href="https://github.com/Kaikygr/omnizap-system" target="_blank" rel="noreferrer noopener"><i class="fa-brands fa-github icon-inline" aria-hidden="true"></i>GitHub</a>
|
|
1062
1177
|
</nav>
|
|
1063
1178
|
</div>
|
|
@@ -1072,6 +1187,8 @@
|
|
|
1072
1187
|
<div class="hero-cta">
|
|
1073
1188
|
<a class="btn primary" href="https://github.com/Kaikygr/omnizap-system" target="_blank" rel="noreferrer noopener"><i class="fa-brands fa-github icon-inline" aria-hidden="true"></i>Ver no GitHub</a>
|
|
1074
1189
|
<a class="btn" href="/api-docs/"><i class="fa-solid fa-book icon-inline" aria-hidden="true"></i>Documentação da API</a>
|
|
1190
|
+
<a id="hero-login-cta" class="btn" href="/login/"><i class="fa-solid fa-right-to-bracket icon-inline" aria-hidden="true"></i>Login</a>
|
|
1191
|
+
<a class="btn" href="/stickers/"><i class="fa-solid fa-icons icon-inline" aria-hidden="true"></i>Ir para Stickers</a>
|
|
1075
1192
|
<a class="btn" href="#infra-arquitetura"><i class="fa-solid fa-diagram-project icon-inline" aria-hidden="true"></i>Ver Arquitetura</a>
|
|
1076
1193
|
</div>
|
|
1077
1194
|
<section class="hero-proof">
|
|
@@ -1269,6 +1386,7 @@ curl -sS https://omnizap.shop/api/sticker-packs/system-summary | jq</code></pre>
|
|
|
1269
1386
|
<div class="foot-col">
|
|
1270
1387
|
<h4><i class="fa-solid fa-cube icon-inline" aria-hidden="true"></i>Produto</h4>
|
|
1271
1388
|
<a href="/"><i class="fa-solid fa-house icon-inline" aria-hidden="true"></i>Home</a>
|
|
1389
|
+
<a href="/login/"><i class="fa-solid fa-right-to-bracket icon-inline" aria-hidden="true"></i>Login</a>
|
|
1272
1390
|
<a href="/stickers/"><i class="fa-solid fa-icons icon-inline" aria-hidden="true"></i>Stickers</a>
|
|
1273
1391
|
<a href="/stickers/create/"><i class="fa-solid fa-plus icon-inline" aria-hidden="true"></i>Criar Pack</a>
|
|
1274
1392
|
<a href="/api-docs/"><i class="fa-solid fa-book icon-inline" aria-hidden="true"></i>API Docs</a>
|
|
@@ -1305,7 +1423,7 @@ curl -sS https://omnizap.shop/api/sticker-packs/system-summary | jq</code></pre>
|
|
|
1305
1423
|
><i class="fa-brands fa-whatsapp" aria-hidden="true"></i></a>
|
|
1306
1424
|
|
|
1307
1425
|
<div id="home-react-root" hidden></div>
|
|
1308
|
-
<script type="module" src="/js/apps/homeApp.js?v=
|
|
1426
|
+
<script type="module" src="/js/apps/homeApp.js?v=20260228-mobile-user-bubble3"></script>
|
|
1309
1427
|
<script type="module" src="/js/github-panel/index.js?v=20260226a"></script>
|
|
1310
1428
|
</body>
|
|
1311
1429
|
</html>
|