@worca/ui 0.9.0-rc.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/styles.css CHANGED
@@ -24,7 +24,6 @@
24
24
  --status-failed: #ef4444;
25
25
  --status-resuming: #3b82f6;
26
26
  --status-skipped: #94a3b8;
27
- --status-interrupted: #f59e0b;
28
27
  /* legacy */
29
28
  --status-in-progress: #3b82f6;
30
29
  --status-error: #ef4444;
@@ -731,7 +730,8 @@ h1, h2, h3, h4, h5, h6 {
731
730
  }
732
731
 
733
732
  .stage-node.status-interrupted .stage-icon {
734
- border-color: var(--status-interrupted);
733
+ border-color: var(--status-in-progress);
734
+ opacity: 0.6;
735
735
  }
736
736
 
737
737
  .stage-label {
@@ -1110,7 +1110,8 @@ sl-details.log-history-panel::part(content) {
1110
1110
  }
1111
1111
 
1112
1112
  .status-interrupted {
1113
- color: var(--status-interrupted);
1113
+ color: var(--status-in-progress);
1114
+ opacity: 0.6;
1114
1115
  }
1115
1116
 
1116
1117
  .status-running {
@@ -4317,205 +4318,3 @@ sl-tooltip.bead-tooltip::part(body) {
4317
4318
  width: 100%;
4318
4319
  }
4319
4320
 
4320
- /* ── Integrations (card catalog) ─────────────────────────────────────── */
4321
-
4322
- .ig-page {
4323
- max-width: 720px;
4324
- }
4325
-
4326
- .ig-subtitle {
4327
- margin: 0 0 1.25rem;
4328
- font-size: 0.85rem;
4329
- color: var(--sl-color-neutral-500);
4330
- }
4331
-
4332
- .ig-cards {
4333
- display: flex;
4334
- flex-direction: column;
4335
- gap: 0.75rem;
4336
- }
4337
-
4338
- .ig-card {
4339
- border: 1px solid var(--sl-color-neutral-200);
4340
- border-radius: var(--sl-border-radius-medium);
4341
- padding: 1rem 1.25rem;
4342
- background: var(--sl-color-neutral-0);
4343
- }
4344
-
4345
- .ig-card--connected {
4346
- border-left: 3px solid var(--sl-color-success-500);
4347
- }
4348
-
4349
- .ig-card--disabled {
4350
- border-left: 3px solid var(--sl-color-neutral-300);
4351
- opacity: 0.7;
4352
- }
4353
-
4354
- .ig-card-footer {
4355
- display: flex;
4356
- justify-content: space-between;
4357
- align-items: center;
4358
- margin-top: 0.75rem;
4359
- padding-left: 2.75rem;
4360
- }
4361
-
4362
- .ig-card--disconnected {
4363
- opacity: 0.85;
4364
- }
4365
-
4366
- .ig-card-header {
4367
- display: flex;
4368
- align-items: center;
4369
- gap: 0.75rem;
4370
- }
4371
-
4372
- .ig-card-icon {
4373
- font-size: 1.5rem;
4374
- line-height: 1;
4375
- }
4376
-
4377
- .ig-card-title {
4378
- flex: 1;
4379
- min-width: 0;
4380
- }
4381
-
4382
- .ig-card-name {
4383
- display: block;
4384
- font-weight: 600;
4385
- font-size: 0.95rem;
4386
- }
4387
-
4388
- .ig-card-desc {
4389
- display: block;
4390
- font-size: 0.8rem;
4391
- color: var(--sl-color-neutral-500);
4392
- }
4393
-
4394
- .ig-badges {
4395
- display: flex;
4396
- flex-direction: column;
4397
- align-items: flex-end;
4398
- gap: 0.25rem;
4399
- }
4400
-
4401
- .ig-card-stats {
4402
- display: flex;
4403
- gap: 1rem;
4404
- margin-top: 0.5rem;
4405
- padding-left: 2.75rem;
4406
- font-size: 0.8rem;
4407
- color: var(--sl-color-neutral-500);
4408
- }
4409
-
4410
- .ig-card-stats .stat-warn {
4411
- color: var(--sl-color-warning-600);
4412
- }
4413
-
4414
- .ig-card-chats {
4415
- display: flex;
4416
- gap: 0.5rem;
4417
- flex-wrap: wrap;
4418
- margin-top: 0.4rem;
4419
- padding-left: 2.75rem;
4420
- }
4421
-
4422
- .ig-chat-badge {
4423
- display: inline-flex;
4424
- align-items: center;
4425
- gap: 0.35rem;
4426
- font-size: 0.8rem;
4427
- }
4428
-
4429
- .ig-chat-id {
4430
- font-family: var(--sl-font-mono);
4431
- color: var(--sl-color-neutral-600);
4432
- font-size: 0.8rem;
4433
- }
4434
-
4435
- .ig-card-actions {
4436
- display: flex;
4437
- gap: 0.5rem;
4438
- margin-top: 0.75rem;
4439
- padding-left: 2.75rem;
4440
- }
4441
-
4442
- /* Inline form */
4443
-
4444
- .ig-form {
4445
- margin-top: 1rem;
4446
- padding: 1rem;
4447
- border-top: 1px solid var(--sl-color-neutral-200);
4448
- }
4449
-
4450
- .ig-env-hint {
4451
- font-size: 0.8rem;
4452
- color: var(--sl-color-neutral-600);
4453
- background: var(--sl-color-neutral-50);
4454
- border: 1px solid var(--sl-color-neutral-200);
4455
- border-radius: var(--sl-border-radius-small);
4456
- padding: 0.5rem 0.75rem;
4457
- margin-bottom: 0.75rem;
4458
- font-family: var(--sl-font-mono);
4459
- }
4460
-
4461
- .ig-form-row {
4462
- display: grid;
4463
- grid-template-columns: 1fr 1fr;
4464
- gap: 0.75rem;
4465
- }
4466
-
4467
- @media (max-width: 600px) {
4468
- .ig-form-row {
4469
- grid-template-columns: 1fr;
4470
- }
4471
- }
4472
-
4473
- .ig-form-field {
4474
- margin-bottom: 0.75rem;
4475
- }
4476
-
4477
- .ig-form-field label {
4478
- display: block;
4479
- font-size: 0.8rem;
4480
- font-weight: 600;
4481
- margin-bottom: 0.2rem;
4482
- color: var(--sl-color-neutral-700);
4483
- }
4484
-
4485
- .ig-form-field .form-hint {
4486
- display: block;
4487
- font-size: 0.7rem;
4488
- color: var(--sl-color-neutral-500);
4489
- margin-top: 0.15rem;
4490
- }
4491
-
4492
- .ig-detect-row {
4493
- display: flex;
4494
- gap: 0.5rem;
4495
- align-items: center;
4496
- }
4497
-
4498
- .ig-detect-row sl-input {
4499
- flex: 1;
4500
- }
4501
-
4502
- .ig-event-grid {
4503
- display: grid;
4504
- grid-template-columns: 1fr 1fr;
4505
- gap: 0.2rem 0.5rem;
4506
- }
4507
-
4508
- .ig-form-actions {
4509
- display: flex;
4510
- gap: 0.5rem;
4511
- margin-top: 0.75rem;
4512
- }
4513
-
4514
- .ig-loading {
4515
- display: flex;
4516
- align-items: center;
4517
- gap: 0.5rem;
4518
- color: var(--sl-color-neutral-500);
4519
- padding: 2rem;
4520
- }
4521
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/ui",
3
- "version": "0.9.0-rc.1",
3
+ "version": "0.9.0",
4
4
  "description": "Pipeline monitoring UI for worca-cc",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
package/server/app.js CHANGED
@@ -2,15 +2,13 @@
2
2
 
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { createHmac, randomUUID } from 'node:crypto';
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { existsSync } from 'node:fs';
6
6
  import { homedir } from 'node:os';
7
7
  import { basename, dirname, isAbsolute, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import express from 'express';
10
10
 
11
11
  import { dbExists, getIssue, listIssues } from './beads-reader.js';
12
- import { RAW_BODY } from './integrations/index.js';
13
- import { verify } from './integrations/verify.js';
14
12
  import { ProcessManager } from './process-manager.js';
15
13
  import { scanDirectory } from './project-registry.js';
16
14
  import {
@@ -18,7 +16,6 @@ import {
18
16
  createProjectScopedRoutes,
19
17
  projectResolver,
20
18
  } from './project-routes.js';
21
- import { validateIntegrationsConfig } from './settings-validator.js';
22
19
  import { discoverSubagents } from './subagents-discovery.js';
23
20
  import { getVersionInfo } from './versions.js';
24
21
  import { createInbox } from './webhook-inbox.js';
@@ -26,25 +23,12 @@ import { createInbox } from './webhook-inbox.js';
26
23
  export function createApp(options = {}) {
27
24
  const app = express();
28
25
  const appDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'app');
29
- const {
30
- settingsPath,
31
- worcaDir,
32
- projectRoot,
33
- prefsDir,
34
- serverHost,
35
- serverPort,
36
- } = options;
26
+ const { settingsPath, worcaDir, projectRoot, prefsDir } = options;
37
27
  // subagentDirs is a test-injection seam; production calls omit it and we
38
28
  // resolve from homedir() + projectRoot.
39
29
  const subagentDirs = options.subagentDirs || null;
40
30
 
41
- app.use(
42
- express.json({
43
- verify: (req, _res, buf) => {
44
- req.rawBody = buf;
45
- },
46
- }),
47
- );
31
+ app.use(express.json());
48
32
 
49
33
  // ─── Security headers ──────────────────────────────────────────────────
50
34
  app.use((_req, res, next) => {
@@ -110,7 +94,7 @@ export function createApp(options = {}) {
110
94
  };
111
95
  next();
112
96
  },
113
- createProjectScopedRoutes({ serverHost, serverPort }),
97
+ createProjectScopedRoutes(),
114
98
  );
115
99
 
116
100
  // ─── Unique routes (not in project-scoped router) ──────────────────────
@@ -298,16 +282,6 @@ export function createApp(options = {}) {
298
282
 
299
283
  // POST /api/webhooks/inbox — receive webhook events
300
284
  app.post('/api/webhooks/inbox', (req, res) => {
301
- const integrations = app.locals.integrations;
302
- if (integrations?.strictInboxVerification) {
303
- const ok = verify(
304
- req.rawBody || Buffer.alloc(0),
305
- req.headers['x-worca-signature'],
306
- integrations.secrets || [],
307
- );
308
- if (!ok)
309
- return res.status(401).json({ ok: false, error: 'invalid signature' });
310
- }
311
285
  const headers = {
312
286
  'x-worca-event': req.headers['x-worca-event'] || '',
313
287
  'x-worca-delivery': req.headers['x-worca-delivery'] || '',
@@ -324,11 +298,9 @@ export function createApp(options = {}) {
324
298
  envelope: req.body || {},
325
299
  projectId,
326
300
  });
327
- if (req.rawBody) stored[RAW_BODY] = req.rawBody;
328
301
  if (app.locals.broadcast) {
329
302
  app.locals.broadcast('webhook-inbox-event', stored);
330
303
  }
331
- app.locals.integrations?.onEvent(stored);
332
304
  res.json({ control: { action: webhookInbox.getControlAction() } });
333
305
  });
334
306
 
@@ -360,16 +332,6 @@ export function createApp(options = {}) {
360
332
 
361
333
  // PUT /api/webhooks/inbox/control — set control action
362
334
  app.put('/api/webhooks/inbox/control', (req, res) => {
363
- const integrations = app.locals.integrations;
364
- if (integrations?.strictInboxVerification) {
365
- const ok = verify(
366
- req.rawBody || Buffer.alloc(0),
367
- req.headers['x-worca-signature'],
368
- integrations.secrets || [],
369
- );
370
- if (!ok)
371
- return res.status(401).json({ ok: false, error: 'invalid signature' });
372
- }
373
335
  const { action } = req.body || {};
374
336
  if (!['continue', 'pause', 'abort'].includes(action)) {
375
337
  return res.status(400).json({
@@ -513,255 +475,14 @@ export function createApp(options = {}) {
513
475
 
514
476
  // ─── Multi-project routes ──────────────────────────────────────────────
515
477
  if (prefsDir) {
516
- app.use(
517
- '/api/projects',
518
- createProjectRoutes({ prefsDir, projectRoot, serverHost, serverPort }),
519
- );
478
+ app.use('/api/projects', createProjectRoutes({ prefsDir, projectRoot }));
520
479
  app.use(
521
480
  '/api/projects/:projectId',
522
481
  projectResolver({ prefsDir, projectRoot }),
523
- createProjectScopedRoutes({ prefsDir, serverHost, serverPort }),
482
+ createProjectScopedRoutes({ prefsDir }),
524
483
  );
525
484
  }
526
485
 
527
- // POST /api/integrations/telegram/detect — find chat IDs from recent messages.
528
- // If the Telegram adapter is running, temporarily pauses its poll loop so
529
- // getUpdates returns results instead of being consumed by the long-poller.
530
- app.post('/api/integrations/telegram/detect', async (req, res) => {
531
- let token = req.body?.token;
532
- if (!token) {
533
- try {
534
- const cfgRaw = readFileSync(
535
- join(prefsDir, 'integrations', 'config.json'),
536
- 'utf8',
537
- );
538
- token = JSON.parse(cfgRaw).telegram?.bot_token;
539
- } catch {
540
- /* no config */
541
- }
542
- }
543
- if (!token) token = process.env.TELEGRAM_BOT_TOKEN;
544
- if (!token) {
545
- return res.status(400).json({ error: 'No bot token provided' });
546
- }
547
-
548
- // Pause the running adapter so getUpdates isn't consumed by the poll loop
549
- const integrations = app.locals.integrations;
550
- const adapterEntry = integrations?._getAdapter?.('telegram');
551
- let wasStopped = false;
552
- if (adapterEntry) {
553
- try {
554
- await adapterEntry.adapter.stop();
555
- wasStopped = true;
556
- // Brief delay to let the in-flight long-poll request complete
557
- await new Promise((r) => setTimeout(r, 200));
558
- } catch {
559
- /* ignore */
560
- }
561
- }
562
-
563
- try {
564
- const meRes = await fetch(`https://api.telegram.org/bot${token}/getMe`);
565
- const me = await meRes.json();
566
- const botUsername = me.ok ? me.result.username : null;
567
-
568
- const updRes = await fetch(
569
- `https://api.telegram.org/bot${token}/getUpdates?timeout=0&limit=20`,
570
- );
571
- const upd = await updRes.json();
572
-
573
- const chats = [];
574
- if (upd.ok) {
575
- for (const u of upd.result) {
576
- const msg = u.message;
577
- if (msg?.chat?.id) {
578
- const existing = chats.find((c) => c.id === msg.chat.id);
579
- if (!existing) {
580
- chats.push({
581
- id: msg.chat.id,
582
- type: msg.chat.type,
583
- title:
584
- msg.chat.title || msg.chat.first_name || String(msg.chat.id),
585
- });
586
- }
587
- }
588
- }
589
- }
590
-
591
- res.json({ ok: true, botUsername, chats });
592
- } catch (err) {
593
- res.status(500).json({ error: err.message });
594
- } finally {
595
- // Restart the adapter if we paused it
596
- if (wasStopped && adapterEntry) {
597
- adapterEntry.adapter.start().catch(() => {});
598
- }
599
- }
600
- });
601
-
602
- // GET /api/integrations/status — adapter states, chat states, counters
603
- app.get('/api/integrations/status', (_req, res) => {
604
- const integrations = app.locals.integrations;
605
- if (!integrations) return res.json({ enabled: false });
606
- res.json(integrations.status());
607
- });
608
-
609
- // GET /api/integrations/config — return saved config (secrets redacted)
610
- app.get('/api/integrations/config', (_req, res) => {
611
- const configPath = join(prefsDir, 'integrations', 'config.json');
612
- let cfg;
613
- try {
614
- cfg = JSON.parse(readFileSync(configPath, 'utf8'));
615
- } catch {
616
- return res.json({});
617
- }
618
- res.json(cfg);
619
- });
620
-
621
- // DELETE /api/integrations/config/:adapter — remove an adapter
622
- // PATCH /api/integrations/config/:adapter/enabled — toggle adapter on/off
623
- app.patch('/api/integrations/config/:adapter/enabled', async (req, res) => {
624
- const { adapter } = req.params;
625
- const { enabled } = req.body;
626
- if (typeof enabled !== 'boolean') {
627
- return res.status(400).json({ error: 'enabled must be a boolean' });
628
- }
629
- const adapterKeys = ['telegram', 'discord', 'slack'];
630
- if (!adapterKeys.includes(adapter)) {
631
- return res.status(400).json({ error: `Invalid adapter: ${adapter}` });
632
- }
633
- const configPath = join(prefsDir, 'integrations', 'config.json');
634
- let cfg;
635
- try {
636
- cfg = JSON.parse(readFileSync(configPath, 'utf8'));
637
- } catch {
638
- return res.status(404).json({ error: 'No integrations config' });
639
- }
640
- if (!cfg[adapter]) {
641
- return res
642
- .status(404)
643
- .json({ error: `Adapter ${adapter} not configured` });
644
- }
645
- cfg[adapter].enabled = enabled;
646
- writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
647
-
648
- // Hot-reload: if disabling, remove the adapter; if enabling, reload it
649
- if (app.locals.ensureIntegrations) app.locals.ensureIntegrations();
650
- if (enabled) {
651
- if (app.locals.integrations?.reloadAdapter) {
652
- await app.locals.integrations.reloadAdapter(adapter);
653
- }
654
- } else {
655
- if (app.locals.integrations?.removeAdapter) {
656
- await app.locals.integrations.removeAdapter(adapter);
657
- }
658
- }
659
- res.json({ ok: true, enabled });
660
- });
661
-
662
- app.delete('/api/integrations/config/:adapter', async (req, res) => {
663
- const { adapter } = req.params;
664
- const adapterKeys = ['telegram', 'discord', 'slack'];
665
- if (!adapterKeys.includes(adapter)) {
666
- return res.status(400).json({ error: `Invalid adapter: ${adapter}` });
667
- }
668
- const configDir = join(prefsDir, 'integrations');
669
- const configPath = join(configDir, 'config.json');
670
- let cfg;
671
- try {
672
- cfg = JSON.parse(readFileSync(configPath, 'utf8'));
673
- } catch {
674
- return res.json({ ok: true });
675
- }
676
- delete cfg[adapter];
677
- const hasAdapters = adapterKeys.some((k) => cfg[k]?.enabled);
678
- if (!hasAdapters) cfg.enabled = false;
679
- try {
680
- writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
681
- } catch (err) {
682
- return res
683
- .status(500)
684
- .json({ error: `Failed to write config: ${err.message}` });
685
- }
686
- if (app.locals.integrations?.removeAdapter) {
687
- await app.locals.integrations.removeAdapter(adapter);
688
- }
689
- res.json({ ok: true });
690
- });
691
-
692
- // POST /api/integrations/config — save adapter config
693
- const ADAPTER_SCHEMA = {
694
- telegram: { tokenKey: 'bot_token', idKey: 'chat_id' },
695
- discord: { tokenKey: 'bot_token', idKey: 'channel_id' },
696
- slack: { tokenKey: 'webhook_url', idKey: 'chat_id' },
697
- };
698
-
699
- app.post('/api/integrations/config', async (req, res) => {
700
- const { adapter, token, chatId, events } = req.body;
701
- if (
702
- !adapter ||
703
- !token ||
704
- !chatId ||
705
- !Array.isArray(events) ||
706
- events.length === 0
707
- ) {
708
- return res.status(400).json({
709
- error: 'Missing required fields: adapter, token, chatId, events',
710
- });
711
- }
712
- const schema = ADAPTER_SCHEMA[adapter];
713
- if (!schema) {
714
- return res.status(400).json({
715
- error: `Invalid adapter: ${adapter}. Must be one of: ${Object.keys(ADAPTER_SCHEMA).join(', ')}`,
716
- });
717
- }
718
-
719
- const configDir = join(prefsDir, 'integrations');
720
- const configPath = join(configDir, 'config.json');
721
-
722
- // Load existing config or start fresh
723
- let cfg = { schema_version: 1, enabled: true };
724
- try {
725
- const raw = readFileSync(configPath, 'utf8');
726
- cfg = JSON.parse(raw);
727
- } catch {
728
- /* start fresh */
729
- }
730
-
731
- // Build adapter block — store token directly in config
732
- const adapterBlock = { enabled: true, events };
733
- adapterBlock[schema.tokenKey] = token;
734
- adapterBlock[schema.idKey] = chatId;
735
-
736
- cfg[adapter] = adapterBlock;
737
- cfg.enabled = true;
738
- if (!cfg.schema_version) cfg.schema_version = 1;
739
-
740
- const result = validateIntegrationsConfig(cfg);
741
- if (!result.valid) {
742
- return res
743
- .status(400)
744
- .json({ error: `Validation failed: ${result.details.join('; ')}` });
745
- }
746
-
747
- try {
748
- mkdirSync(configDir, { recursive: true });
749
- writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
750
- } catch (err) {
751
- return res
752
- .status(500)
753
- .json({ error: `Failed to write config: ${err.message}` });
754
- }
755
-
756
- // Hot-reload just this adapter (no full restart)
757
- if (app.locals.ensureIntegrations) app.locals.ensureIntegrations();
758
- if (app.locals.integrations?.reloadAdapter) {
759
- await app.locals.integrations.reloadAdapter(adapter);
760
- }
761
-
762
- res.json({ ok: true, path: configPath });
763
- });
764
-
765
486
  // ─── Dynamic favicon ──────────────────────────────────────────────────
766
487
  // Serve mode-specific favicon before express.static so it takes precedence.
767
488
  app.get('/favicon.svg', (_req, res) => {
package/server/index.js CHANGED
@@ -4,7 +4,6 @@ import { createServer } from 'node:http';
4
4
  import { homedir, platform } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { createApp } from './app.js';
7
- import { createIntegrations } from './integrations/index.js';
8
7
  import { attachWsServer } from './ws.js';
9
8
 
10
9
  // Parse argv
@@ -58,8 +57,6 @@ const app = createApp({
58
57
  projectRoot,
59
58
  webhookInbox,
60
59
  prefsDir,
61
- serverHost: host,
62
- serverPort: port,
63
60
  });
64
61
  const server = createServer(app);
65
62
 
@@ -107,25 +104,6 @@ app.locals.broadcast = broadcast;
107
104
  app.locals.scheduleRefresh = scheduleRefresh;
108
105
  app.locals.resolveRunProject = resolveRunProject;
109
106
 
110
- // Boot chat integrations only in global mode — project-scoped instances skip
111
- // integrations to avoid duplicate Telegram long-poll connections on the same bot.
112
- if (isGlobal) {
113
- const integrationsOpts = {
114
- port,
115
- host,
116
- prefsDir,
117
- configPath: join(prefsDir, 'integrations', 'config.json'),
118
- };
119
- app.locals.integrations = createIntegrations(integrationsOpts);
120
- app.locals.ensureIntegrations = () => {
121
- if (!app.locals.integrations?.reloadAdapter) {
122
- app.locals.integrations = createIntegrations(integrationsOpts);
123
- }
124
- };
125
- } else {
126
- console.log('[integrations] Skipped — integrations only run in global mode');
127
- }
128
-
129
107
  // ─── worca-cc version check (non-blocking) ─────────────────────────────
130
108
  checkWorcaVersion().then((result) => {
131
109
  app.locals.worcaVersion = result;