@habeetat/cli 0.1.0-dev.20260330090152.e7723de → 0.1.0-dev.20260330110637.de8c473
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/bin.js +197 -0
- package/dist/bin.js.map +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +37 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -52,6 +52,7 @@ var SERVICES = [
|
|
|
52
52
|
"launcher",
|
|
53
53
|
"organization-manager",
|
|
54
54
|
"platform-manager",
|
|
55
|
+
"analytics",
|
|
55
56
|
"sample-crm",
|
|
56
57
|
"nginx",
|
|
57
58
|
"zookeeper",
|
|
@@ -258,6 +259,7 @@ function buildSeedLogtoAppsSql(env) {
|
|
|
258
259
|
const launcherUrl = env.LAUNCHER_URL || `${protocol}://launcher.${domain}`;
|
|
259
260
|
const orgManagerUrl = env.ORG_MANAGER_URL || `${protocol}://organization-manager.${domain}`;
|
|
260
261
|
const platformManagerUrl = env.PLATFORM_MANAGER_URL || `${protocol}://platform-manager.${domain}`;
|
|
262
|
+
const analyticsUrl = env.ANALYTICS_URL || `${protocol}://analytics.${domain}`;
|
|
261
263
|
const sampleCrmUrl = env.SAMPLE_CRM_URL || `${protocol}://sample-crm.${domain}`;
|
|
262
264
|
const apiResource = env.IAM_AUDIENCE || `${protocol}://api.${domain}/api`;
|
|
263
265
|
const m2mAppId = "nhp-m2m-config";
|
|
@@ -300,6 +302,14 @@ VALUES (
|
|
|
300
302
|
'{}'::jsonb, false
|
|
301
303
|
) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, type = EXCLUDED.type, oidc_client_metadata = EXCLUDED.oidc_client_metadata;
|
|
302
304
|
|
|
305
|
+
-- Analytics SPA
|
|
306
|
+
INSERT INTO applications (tenant_id, id, name, secret, description, type, oidc_client_metadata, custom_client_metadata, is_third_party)
|
|
307
|
+
VALUES (
|
|
308
|
+
'default', 'nhp-analytics', 'Habeetat Analytics', 'spa-secret-nhp-analytics', 'Platform analytics dashboard', 'SPA',
|
|
309
|
+
jsonb_build_object('redirectUris', ARRAY['${analyticsUrl}', '${analyticsUrl}/callback'], 'postLogoutRedirectUris', ARRAY['${analyticsUrl}']),
|
|
310
|
+
'{}'::jsonb, false
|
|
311
|
+
) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, type = EXCLUDED.type, oidc_client_metadata = EXCLUDED.oidc_client_metadata;
|
|
312
|
+
|
|
303
313
|
-- Sample CRM SPA
|
|
304
314
|
INSERT INTO applications (tenant_id, id, name, secret, description, type, oidc_client_metadata, custom_client_metadata, is_third_party)
|
|
305
315
|
VALUES (
|
|
@@ -364,6 +374,7 @@ VALUES
|
|
|
364
374
|
('app_launcher', 'launcher', 'Habeetat Launcher', 'Main launcher and navigation hub', 'system', true, true, true, true, 'PUBLISHED', 'nhp-launcher', NOW(), NOW()),
|
|
365
375
|
('app_org_manager', 'organization-manager', 'Organization Manager', 'Manage organizations, users and roles', 'system', true, true, false, true, 'PUBLISHED', 'nhp-org-manager', NOW(), NOW()),
|
|
366
376
|
('app_plat_manager', 'platform-manager', 'Platform Manager', 'Platform administration and configuration', 'system', true, true, true, false, 'PUBLISHED', 'nhp-plat-mgr', NOW(), NOW()),
|
|
377
|
+
('app_analytics', 'analytics', 'Habeetat Analytics', 'Platform analytics dashboard', 'system', true, true, true, false, 'PUBLISHED', 'nhp-analytics', NOW(), NOW()),
|
|
367
378
|
('app_sample_crm', 'sample-crm', 'Sample CRM', 'Sample CRM application for demonstration', 'demo', true, false, false, false, 'PUBLISHED', 'nhp-sample-crm', NOW(), NOW())
|
|
368
379
|
ON CONFLICT (slug) DO UPDATE SET
|
|
369
380
|
name = EXCLUDED.name,
|
|
@@ -380,6 +391,7 @@ VALUES
|
|
|
380
391
|
('ver_launcher_100', 'app_launcher', '1.0.0', jsonb_build_object('endpoints', jsonb_build_object('main', '${protocol}://launcher.${domain}')), 'ACTIVE', NOW()),
|
|
381
392
|
('ver_org_manager_100', 'app_org_manager', '1.0.0', jsonb_build_object('endpoints', jsonb_build_object('main', '${protocol}://organization-manager.${domain}')), 'ACTIVE', NOW()),
|
|
382
393
|
('ver_plat_manager_100', 'app_plat_manager', '1.0.0', jsonb_build_object('endpoints', jsonb_build_object('main', '${protocol}://platform-manager.${domain}')), 'ACTIVE', NOW()),
|
|
394
|
+
('ver_analytics_100', 'app_analytics', '1.0.0', jsonb_build_object('endpoints', jsonb_build_object('main', '${protocol}://analytics.${domain}')), 'ACTIVE', NOW()),
|
|
383
395
|
('ver_sample_crm_100', 'app_sample_crm', '1.0.0', jsonb_build_object('endpoints', jsonb_build_object('main', '${protocol}://sample-crm.${domain}')), 'ACTIVE', NOW())
|
|
384
396
|
ON CONFLICT (app_id, version) DO UPDATE SET manifest = EXCLUDED.manifest;
|
|
385
397
|
|
|
@@ -455,6 +467,145 @@ JOIN application_versions v ON v.app_id = a.id AND v.version = '1.0.0'
|
|
|
455
467
|
WHERE t.slug = '${orgSlug}'
|
|
456
468
|
ON CONFLICT (tenant_id, app_id) DO NOTHING;`;
|
|
457
469
|
}
|
|
470
|
+
function buildClickhouseAnalyticsSeedSql(tenantId, adminUserId) {
|
|
471
|
+
const now = /* @__PURE__ */ new Date();
|
|
472
|
+
const rows = [];
|
|
473
|
+
const users = [adminUserId, "usr_demo_001", "usr_demo_002", "usr_demo_003", "usr_demo_004"];
|
|
474
|
+
const events = [
|
|
475
|
+
{ name: "page_view", props: (page) => `{"page":"${page}","referrer":"direct"}` },
|
|
476
|
+
{ name: "user_login", props: () => `{"method":"password"}` },
|
|
477
|
+
{ name: "feature_used", props: (f) => `{"feature":"${f}"}` },
|
|
478
|
+
{ name: "report_created", props: () => `{"type":"insight"}` },
|
|
479
|
+
{ name: "dashboard_viewed", props: () => `{"dashboard":"main"}` },
|
|
480
|
+
{ name: "user_logout", props: () => `{}` }
|
|
481
|
+
];
|
|
482
|
+
const pages = ["/dashboard", "/analytics", "/users", "/settings", "/reports"];
|
|
483
|
+
const features = ["insights", "funnels", "retention", "user-feed", "dashboards"];
|
|
484
|
+
for (let day = 29; day >= 0; day--) {
|
|
485
|
+
const date = new Date(now);
|
|
486
|
+
date.setDate(date.getDate() - day);
|
|
487
|
+
const eventsPerDay = 4 + Math.floor(Math.random() * 10);
|
|
488
|
+
for (let i = 0; i < eventsPerDay; i++) {
|
|
489
|
+
const userId = users[Math.floor(Math.random() * users.length)];
|
|
490
|
+
const hour = 8 + Math.floor(Math.random() * 10);
|
|
491
|
+
const minute = Math.floor(Math.random() * 60);
|
|
492
|
+
const ts = `${date.toISOString().split("T")[0]} ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00`;
|
|
493
|
+
const sessionId = `sess_${day}_${i}`;
|
|
494
|
+
if (i === 0) {
|
|
495
|
+
rows.push(`('${tenantId}','${userId}','${sessionId}','user_login','{"method":"password"}','${ts}')`);
|
|
496
|
+
}
|
|
497
|
+
const eventIdx = Math.floor(Math.random() * events.length);
|
|
498
|
+
const ev = events[eventIdx];
|
|
499
|
+
let props = "";
|
|
500
|
+
if (ev.name === "page_view") props = ev.props(pages[Math.floor(Math.random() * pages.length)]);
|
|
501
|
+
else if (ev.name === "feature_used") props = ev.props(features[Math.floor(Math.random() * features.length)]);
|
|
502
|
+
else props = ev.props();
|
|
503
|
+
rows.push(`('${tenantId}','${userId}','${sessionId}','${ev.name}','${props}','${ts}')`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return `INSERT INTO nhp_analytics.events (tenant_id, user_id, session_id, event_name, properties, timestamp) VALUES ${rows.join(",\n")};`;
|
|
507
|
+
}
|
|
508
|
+
function buildAnalyticsObjectsSql(tenantId, adminUserId) {
|
|
509
|
+
return `
|
|
510
|
+
-- Analytics dashboard
|
|
511
|
+
INSERT INTO analytics_dashboards (id, tenant_id, name, description, layout, created_by, created_at, updated_at)
|
|
512
|
+
VALUES (
|
|
513
|
+
'dash_default_${tenantId.replace(/-/g, "_")}',
|
|
514
|
+
'${tenantId}',
|
|
515
|
+
'Overview',
|
|
516
|
+
'Main analytics dashboard',
|
|
517
|
+
'[{"i":"ins_pv","x":0,"y":0,"w":6,"h":3},{"i":"ins_fl","x":6,"y":0,"w":6,"h":3},{"i":"ins_fu","x":0,"y":3,"w":12,"h":3}]',
|
|
518
|
+
'${adminUserId}',
|
|
519
|
+
NOW(), NOW()
|
|
520
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
521
|
+
|
|
522
|
+
-- Insight: Page views trend
|
|
523
|
+
INSERT INTO analytics_insights (id, tenant_id, name, type, config, created_by, created_at, updated_at)
|
|
524
|
+
VALUES (
|
|
525
|
+
'ins_pv',
|
|
526
|
+
'${tenantId}',
|
|
527
|
+
'Page Views (last 30d)',
|
|
528
|
+
'TREND',
|
|
529
|
+
'{"eventName":"page_view","aggregation":"count","interval":"day","filters":[]}',
|
|
530
|
+
'${adminUserId}',
|
|
531
|
+
NOW(), NOW()
|
|
532
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
533
|
+
|
|
534
|
+
-- Insight: Feature usage breakdown
|
|
535
|
+
INSERT INTO analytics_insights (id, tenant_id, name, type, config, created_by, created_at, updated_at)
|
|
536
|
+
VALUES (
|
|
537
|
+
'ins_fl',
|
|
538
|
+
'${tenantId}',
|
|
539
|
+
'Feature Usage Breakdown',
|
|
540
|
+
'PIE',
|
|
541
|
+
'{"eventName":"feature_used","aggregation":"count","interval":"day","groupBy":"feature","filters":[]}',
|
|
542
|
+
'${adminUserId}',
|
|
543
|
+
NOW(), NOW()
|
|
544
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
545
|
+
|
|
546
|
+
-- Insight: Unique active users
|
|
547
|
+
INSERT INTO analytics_insights (id, tenant_id, name, type, config, created_by, created_at, updated_at)
|
|
548
|
+
VALUES (
|
|
549
|
+
'ins_fu',
|
|
550
|
+
'${tenantId}',
|
|
551
|
+
'Unique Active Users (last 30d)',
|
|
552
|
+
'TREND',
|
|
553
|
+
'{"eventName":"user_login","aggregation":"count_unique","interval":"day","filters":[]}',
|
|
554
|
+
'${adminUserId}',
|
|
555
|
+
NOW(), NOW()
|
|
556
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
557
|
+
|
|
558
|
+
-- Funnel: Login \u2192 Feature \u2192 Report
|
|
559
|
+
INSERT INTO analytics_funnels (id, tenant_id, name, steps, window_seconds, created_by, created_at, updated_at)
|
|
560
|
+
VALUES (
|
|
561
|
+
'fun_onboarding_${tenantId.replace(/-/g, "_")}',
|
|
562
|
+
'${tenantId}',
|
|
563
|
+
'Login \u2192 Feature \u2192 Report',
|
|
564
|
+
'[{"eventName":"user_login"},{"eventName":"feature_used"},{"eventName":"report_created"}]',
|
|
565
|
+
86400,
|
|
566
|
+
'${adminUserId}',
|
|
567
|
+
NOW(), NOW()
|
|
568
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
569
|
+
|
|
570
|
+
-- Funnel: Login \u2192 Dashboard \u2192 Feature
|
|
571
|
+
INSERT INTO analytics_funnels (id, tenant_id, name, steps, window_seconds, created_by, created_at, updated_at)
|
|
572
|
+
VALUES (
|
|
573
|
+
'fun_engagement_${tenantId.replace(/-/g, "_")}',
|
|
574
|
+
'${tenantId}',
|
|
575
|
+
'Login \u2192 Dashboard \u2192 Feature',
|
|
576
|
+
'[{"eventName":"user_login"},{"eventName":"dashboard_viewed"},{"eventName":"feature_used"}]',
|
|
577
|
+
3600,
|
|
578
|
+
'${adminUserId}',
|
|
579
|
+
NOW(), NOW()
|
|
580
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
581
|
+
|
|
582
|
+
-- Retention: Login \u2192 return login (weekly)
|
|
583
|
+
INSERT INTO analytics_retentions (id, tenant_id, name, start_event, return_event, period, created_by, created_at, updated_at)
|
|
584
|
+
VALUES (
|
|
585
|
+
'ret_login_${tenantId.replace(/-/g, "_")}',
|
|
586
|
+
'${tenantId}',
|
|
587
|
+
'Weekly Login Retention',
|
|
588
|
+
'user_login',
|
|
589
|
+
'user_login',
|
|
590
|
+
'WEEK',
|
|
591
|
+
'${adminUserId}',
|
|
592
|
+
NOW(), NOW()
|
|
593
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
594
|
+
|
|
595
|
+
-- Retention: Feature adoption (daily)
|
|
596
|
+
INSERT INTO analytics_retentions (id, tenant_id, name, start_event, return_event, period, created_by, created_at, updated_at)
|
|
597
|
+
VALUES (
|
|
598
|
+
'ret_feature_${tenantId.replace(/-/g, "_")}',
|
|
599
|
+
'${tenantId}',
|
|
600
|
+
'Feature Adoption Retention',
|
|
601
|
+
'feature_used',
|
|
602
|
+
'feature_used',
|
|
603
|
+
'DAY',
|
|
604
|
+
'${adminUserId}',
|
|
605
|
+
NOW(), NOW()
|
|
606
|
+
) ON CONFLICT (id) DO NOTHING;
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
458
609
|
async function initPlatform(projectDir) {
|
|
459
610
|
const config = loadConfig(projectDir);
|
|
460
611
|
const env = loadEnv(projectDir);
|
|
@@ -481,6 +632,23 @@ async function initPlatform(projectDir) {
|
|
|
481
632
|
throw new Error("platform-db timeout");
|
|
482
633
|
}
|
|
483
634
|
dbSpinner.succeed("Databases ready");
|
|
635
|
+
const clickhouseSpinner = ora__default.default("Initializing ClickHouse analytics schema...").start();
|
|
636
|
+
try {
|
|
637
|
+
const clickhouseSql = [
|
|
638
|
+
"CREATE DATABASE IF NOT EXISTS nhp_analytics",
|
|
639
|
+
`CREATE TABLE IF NOT EXISTS nhp_analytics.events (event_id UUID DEFAULT generateUUIDv4(), tenant_id String, user_id String, session_id String DEFAULT '', event_name LowCardinality(String), properties String DEFAULT '{}', timestamp DateTime64(3, 'UTC'), inserted_at DateTime DEFAULT now()) ENGINE = MergeTree PARTITION BY toYYYYMM(timestamp) ORDER BY (tenant_id, event_name, timestamp, user_id) TTL toDateTime(timestamp) + INTERVAL 365 DAY DELETE SETTINGS index_granularity = 8192`,
|
|
640
|
+
"ALTER TABLE nhp_analytics.events ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter(0.01) GRANULARITY 4",
|
|
641
|
+
"ALTER TABLE nhp_analytics.events ADD INDEX IF NOT EXISTS idx_session_id session_id TYPE bloom_filter(0.01) GRANULARITY 4"
|
|
642
|
+
];
|
|
643
|
+
for (const sql of clickhouseSql) {
|
|
644
|
+
await execCompose(projectDir, `exec -T clickhouse clickhouse-client --query "${sql}"`);
|
|
645
|
+
}
|
|
646
|
+
clickhouseSpinner.succeed("ClickHouse analytics schema initialized");
|
|
647
|
+
} catch (err) {
|
|
648
|
+
clickhouseSpinner.warn("ClickHouse schema initialization failed \u2014 analytics may not work");
|
|
649
|
+
logger.error(String(err));
|
|
650
|
+
logger.info('Run manually: docker compose exec clickhouse clickhouse-client --query "CREATE DATABASE IF NOT EXISTS nhp_analytics"');
|
|
651
|
+
}
|
|
484
652
|
const logtoSpinner = ora__default.default("Waiting for Logto to be healthy (this may take up to 2 min)...").start();
|
|
485
653
|
const logtoReady = await waitForService(
|
|
486
654
|
projectDir,
|
|
@@ -630,6 +798,32 @@ async function initPlatform(projectDir) {
|
|
|
630
798
|
logger.error(String(err));
|
|
631
799
|
logger.info("You may need to initialize the platform manually after first login");
|
|
632
800
|
}
|
|
801
|
+
const analyticsSpinner = ora__default.default("Seeding analytics demo data...").start();
|
|
802
|
+
try {
|
|
803
|
+
const orgName = env.ORGANIZATION_NAME || config.platform.organization || "Default";
|
|
804
|
+
const orgSlug = orgName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
805
|
+
const adminUserId = "usr_plat_admin01";
|
|
806
|
+
const chSql = buildClickhouseAnalyticsSeedSql(orgSlug, adminUserId);
|
|
807
|
+
await execa.execa("docker", [
|
|
808
|
+
"compose",
|
|
809
|
+
"-f",
|
|
810
|
+
COMPOSE_FILE,
|
|
811
|
+
"exec",
|
|
812
|
+
"-T",
|
|
813
|
+
"clickhouse",
|
|
814
|
+
"clickhouse-client",
|
|
815
|
+
"--multiquery"
|
|
816
|
+
], { cwd: projectDir, input: chSql, stdio: ["pipe", "pipe", "pipe"] });
|
|
817
|
+
const pgSql = buildAnalyticsObjectsSql(orgSlug, adminUserId);
|
|
818
|
+
await execa.execaCommand(
|
|
819
|
+
`docker compose -f ${COMPOSE_FILE} exec -T platform-db psql -U ${env.PLATFORM_DB_USER || "habeetat"} -d ${env.PLATFORM_DB_NAME || "habeetat_platform"}`,
|
|
820
|
+
{ cwd: projectDir, input: pgSql }
|
|
821
|
+
);
|
|
822
|
+
analyticsSpinner.succeed("Analytics demo data seeded");
|
|
823
|
+
} catch (err) {
|
|
824
|
+
analyticsSpinner.warn("Analytics demo data seeding failed \u2014 app will start empty");
|
|
825
|
+
logger.error(String(err));
|
|
826
|
+
}
|
|
633
827
|
if (config.services.signoz) {
|
|
634
828
|
const signozUserSpinner = ora__default.default("Creating SigNoz admin user...").start();
|
|
635
829
|
try {
|
|
@@ -716,6 +910,9 @@ async function upCommand() {
|
|
|
716
910
|
console.log(` Launcher: ${proto}://launcher.${domain}`);
|
|
717
911
|
console.log(` Organization Manager: ${proto}://organization-manager.${domain}`);
|
|
718
912
|
console.log(` Platform Manager: ${proto}://platform-manager.${domain}`);
|
|
913
|
+
if (config.services.analytics) {
|
|
914
|
+
console.log(` Analytics: ${proto}://analytics.${domain}`);
|
|
915
|
+
}
|
|
719
916
|
console.log(` Backend API: ${proto}://api.${domain}`);
|
|
720
917
|
console.log(` Logto IAM: ${proto}://iam.${domain}`);
|
|
721
918
|
console.log(` Logto Admin Console: ${proto}://iam-console.${domain}`);
|