@serve.zone/dcrouter 16.3.0 → 16.4.1

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.
@@ -79,6 +79,7 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
79
79
  /**
80
80
  * Current status of the email
81
81
  */
82
+ @plugins.smartdata.index()
82
83
  @plugins.smartdata.svDb()
83
84
  public status!: TCachedEmailStatus;
84
85
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '16.3.0',
6
+ version: '16.4.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -25,7 +25,7 @@ declare global {
25
25
 
26
26
  @customElement('ops-view-redirects')
27
27
  export class OpsViewRedirects extends DeesElement {
28
- @property({ type: Boolean })
28
+ @property({ type: Boolean, reflect: true })
29
29
  accessor embedded = false;
30
30
  @property({ type: String })
31
31
  accessor ownerFilter: TRouteOwnerFilter = 'all';
@@ -59,6 +59,12 @@ export class OpsViewRedirects extends DeesElement {
59
59
  cssManager.defaultStyles,
60
60
  viewHostCss,
61
61
  css`
62
+ :host([embedded]) {
63
+ margin: 0;
64
+ max-width: none;
65
+ padding: 0;
66
+ }
67
+
62
68
  .redirectsContainer {
63
69
  display: flex;
64
70
  flex-direction: column;
@@ -137,7 +143,9 @@ export class OpsViewRedirects extends DeesElement {
137
143
  return html`
138
144
  ${this.embedded ? '' : html`<dees-heading level="3">Redirects</dees-heading>`}
139
145
  <div class="redirectsContainer">
140
- <dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
146
+ ${this.embedded
147
+ ? ''
148
+ : html`<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>`}
141
149
 
142
150
  ${redirects.length > 0
143
151
  ? html`
@@ -158,14 +166,7 @@ export class OpsViewRedirects extends DeesElement {
158
166
  'Covered By': redirect.coveredByRouteNames.join(', ') || '-',
159
167
  Notes: this.formatNotes(redirect),
160
168
  })}
161
- .dataActions=${[
162
- {
163
- name: 'Refresh',
164
- iconName: 'lucide:RefreshCw',
165
- type: ['header' as const],
166
- actionFunc: async () => this.refreshData(),
167
- },
168
- ]}
169
+ .dataActions=${this.getTableActions()}
169
170
  ></dees-table>
170
171
  `
171
172
  : html`
@@ -174,14 +175,7 @@ export class OpsViewRedirects extends DeesElement {
174
175
  .heading2=${'Runtime redirects derived from enabled HTTPS routes'}
175
176
  .data=${[]}
176
177
  .displayFunction=${() => ({})}
177
- .dataActions=${[
178
- {
179
- name: 'Refresh',
180
- iconName: 'lucide:RefreshCw',
181
- type: ['header' as const],
182
- actionFunc: async () => this.refreshData(),
183
- },
184
- ]}
178
+ .dataActions=${this.getTableActions()}
185
179
  ></dees-table>
186
180
  <div class="empty-state">
187
181
  <p>No derived redirects</p>
@@ -196,6 +190,19 @@ export class OpsViewRedirects extends DeesElement {
196
190
  await appstate.routeManagementStatePart.dispatchAction(appstate.fetchHttpRedirectsAction, null);
197
191
  }
198
192
 
193
+ private getTableActions() {
194
+ return this.embedded
195
+ ? []
196
+ : [
197
+ {
198
+ name: 'Refresh',
199
+ iconName: 'lucide:refreshCw',
200
+ type: ['header' as const],
201
+ actionFunc: async () => this.refreshData(),
202
+ },
203
+ ];
204
+ }
205
+
199
206
  private formatStatus(status: interfaces.data.THttpRedirectStatus): string {
200
207
  return status.charAt(0).toUpperCase() + status.slice(1);
201
208
  }
@@ -9,9 +9,14 @@ import './ops-view-redirects.js';
9
9
  import './ops-view-special-forwards.js';
10
10
  import {
11
11
  getRouteOwner,
12
+ redirectMatchesOwner,
12
13
  routeMatchesOwner,
13
14
  type TRouteOwnerFilter,
14
15
  } from './route-view-filters.js';
16
+ import {
17
+ letsEncryptHttp01ManagedRouteKind,
18
+ showLetsEncryptHttp01ForwardDialog,
19
+ } from './special-forward-dialog.js';
15
20
  import type {
16
21
  IRoutePathClassOption as ISzRoutePathClassOption,
17
22
  IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
@@ -387,8 +392,7 @@ export class OpsViewRoutes extends DeesElement {
387
392
  .select((s) => s.isLoggedIn)
388
393
  .subscribe((isLoggedIn) => {
389
394
  if (isLoggedIn) {
390
- appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
391
- appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
395
+ void this.refreshData();
392
396
  }
393
397
  });
394
398
  this.rxSubscriptions.push(loginSub);
@@ -416,12 +420,13 @@ export class OpsViewRoutes extends DeesElement {
416
420
 
417
421
  .routeFilters {
418
422
  display: flex;
419
- align-items: flex-start;
423
+ align-items: flex-end;
420
424
  gap: 16px;
421
- flex-wrap: wrap;
425
+ flex-wrap: nowrap;
422
426
  }
423
427
 
424
428
  .routeFilter {
429
+ flex: 0 1 auto;
425
430
  min-width: 0;
426
431
  max-width: 100%;
427
432
  overflow-x: auto;
@@ -430,6 +435,11 @@ export class OpsViewRoutes extends DeesElement {
430
435
  @media (max-width: 760px) {
431
436
  .routeFilters {
432
437
  flex-direction: column;
438
+ align-items: stretch;
439
+ }
440
+
441
+ .routeFilter {
442
+ width: 100%;
433
443
  }
434
444
  }
435
445
 
@@ -468,55 +478,24 @@ export class OpsViewRoutes extends DeesElement {
468
478
  public render(): TemplateResult {
469
479
  const { mergedRoutes, warnings } = this.routeState;
470
480
  const standardRoutes = mergedRoutes.filter((route) => !route.metadata?.managedRouteKind);
471
- const filteredRoutes = standardRoutes.filter((route) => {
481
+ const filteredStandardRoutes = standardRoutes.filter((route) => {
472
482
  return routeMatchesOwner(route, this.ownerFilter);
473
483
  });
474
-
475
- const disabledCount = filteredRoutes.filter((route) => !route.enabled).length;
476
- const systemCount = filteredRoutes.filter((route) => getRouteOwner(route) === 'system').length;
477
- const userCount = filteredRoutes.filter((route) => getRouteOwner(route) === 'operator').length;
478
-
479
- const statsTiles: IStatsTile[] = [
480
- {
481
- id: 'totalRoutes',
482
- title: 'Standard Routes',
483
- type: 'number',
484
- value: filteredRoutes.length,
485
- icon: 'lucide:route',
486
- description: 'Matching non-special routes',
487
- color: '#3b82f6',
488
- },
489
- {
490
- id: 'configRoutes',
491
- title: 'System Routes',
492
- type: 'number',
493
- value: systemCount,
494
- icon: 'lucide:settings',
495
- description: 'From config, email, and DNS',
496
- color: '#8b5cf6',
497
- },
498
- {
499
- id: 'userRoutes',
500
- title: 'User Routes',
501
- type: 'number',
502
- value: userCount,
503
- icon: 'lucide:code',
504
- description: 'Created directly by users',
505
- color: '#0ea5e9',
506
- },
507
- {
508
- id: 'disabled',
509
- title: 'Disabled',
510
- type: 'number',
511
- value: disabledCount,
512
- icon: 'lucide:pauseCircle',
513
- description: 'Currently disabled routes',
514
- color: disabledCount > 0 ? '#ef4444' : '#6b7280',
515
- },
516
- ];
484
+ const filteredSpecialRoutes = mergedRoutes.filter((route) => {
485
+ return route.metadata?.managedRouteKind === letsEncryptHttp01ManagedRouteKind
486
+ && routeMatchesOwner(route, this.ownerFilter);
487
+ });
488
+ const filteredRedirects = (this.routeState.httpRedirects || []).filter((redirect) => {
489
+ return redirectMatchesOwner(redirect, mergedRoutes, this.ownerFilter);
490
+ });
491
+ const statsTiles = this.getStatsTiles(
492
+ filteredStandardRoutes,
493
+ filteredSpecialRoutes,
494
+ filteredRedirects,
495
+ );
517
496
 
518
497
  // Map filtered routes to sz-route-list-view format
519
- const szRoutes = filteredRoutes.map((mr) => {
498
+ const szRoutes = filteredStandardRoutes.map((mr) => {
520
499
  const tags = [...(mr.route.tags || [])];
521
500
  tags.push(mr.origin);
522
501
  if (!mr.enabled) tags.push('disabled');
@@ -554,26 +533,17 @@ export class OpsViewRoutes extends DeesElement {
554
533
  </div>
555
534
  </div>
556
535
 
536
+ <dees-statsgrid
537
+ id="routeStats"
538
+ .tiles=${statsTiles}
539
+ .gridActions=${this.getGridActions()}
540
+ ></dees-statsgrid>
541
+
557
542
  ${this.activeSection === 'all' || this.activeSection === 'standard'
558
543
  ? html`
559
544
  ${this.activeSection === 'all'
560
545
  ? html`<dees-heading level="4">Standard Routes</dees-heading>`
561
546
  : ''}
562
- <dees-statsgrid
563
- .tiles=${statsTiles}
564
- .gridActions=${[
565
- {
566
- name: 'Add Route',
567
- iconName: 'lucide:plus',
568
- action: () => this.showCreateRouteDialog(),
569
- },
570
- {
571
- name: 'Refresh',
572
- iconName: 'lucide:refreshCw',
573
- action: () => this.refreshData(),
574
- },
575
- ]}
576
- ></dees-statsgrid>
577
547
 
578
548
  ${warnings.length > 0
579
549
  ? html`
@@ -590,29 +560,29 @@ export class OpsViewRoutes extends DeesElement {
590
560
  `
591
561
  : ''}
592
562
 
593
- ${szRoutes.length > 0
563
+ <sz-route-list-view
564
+ .routes=${szRoutes}
565
+ .showActionsFilter=${(route: { id?: string; name?: string }) =>
566
+ this.canEditRoute(route)}
567
+ @route-click=${(event: CustomEvent) => this.handleRouteClick(event)}
568
+ @route-edit=${(event: CustomEvent) => this.handleRouteEdit(event)}
569
+ @route-delete=${(event: CustomEvent) => this.handleRouteDelete(event)}
570
+ ></sz-route-list-view>
571
+ ${szRoutes.length === 0
594
572
  ? html`
595
- <sz-route-list-view
596
- .routes=${szRoutes}
597
- .showActionsFilter=${(route: { id?: string; name?: string }) =>
598
- this.canEditRoute(route)}
599
- @route-click=${(event: CustomEvent) => this.handleRouteClick(event)}
600
- @route-edit=${(event: CustomEvent) => this.handleRouteEdit(event)}
601
- @route-delete=${(event: CustomEvent) => this.handleRouteDelete(event)}
602
- ></sz-route-list-view>
603
- `
604
- : html`
605
573
  <div class="empty-state">
606
574
  <p>No standard routes match these filters</p>
607
- <p>Choose another ownership filter or add a user route.</p>
575
+ <p>Choose another ownership filter or add a User Route.</p>
608
576
  </div>
609
- `}
577
+ `
578
+ : ''}
610
579
  `
611
580
  : ''}
612
581
 
613
582
  ${this.activeSection === 'all' || this.activeSection === 'special'
614
583
  ? html`
615
584
  <ops-view-special-forwards
585
+ .embedded=${true}
616
586
  .ownerFilter=${this.ownerFilter}
617
587
  ></ops-view-special-forwards>
618
588
  `
@@ -630,6 +600,170 @@ export class OpsViewRoutes extends DeesElement {
630
600
  `;
631
601
  }
632
602
 
603
+ private getStatsTiles(
604
+ standardRoutes: interfaces.data.IMergedRoute[],
605
+ specialRoutes: interfaces.data.IMergedRoute[],
606
+ redirects: interfaces.data.IHttpRedirectInfo[],
607
+ ): IStatsTile[] {
608
+ if (this.activeSection === 'all') {
609
+ const disabledCount = [...standardRoutes, ...specialRoutes]
610
+ .filter((route) => !route.enabled).length;
611
+ return [
612
+ {
613
+ id: 'allStandardRoutes',
614
+ title: 'Standard',
615
+ type: 'number',
616
+ value: standardRoutes.length,
617
+ icon: 'lucide:route',
618
+ description: 'Matching standard routes',
619
+ color: '#3b82f6',
620
+ },
621
+ {
622
+ id: 'allSpecialRoutes',
623
+ title: 'Special',
624
+ type: 'number',
625
+ value: specialRoutes.length,
626
+ icon: 'lucide:wandSparkles',
627
+ description: 'Matching managed special forwards',
628
+ color: '#8b5cf6',
629
+ },
630
+ {
631
+ id: 'allRedirects',
632
+ title: 'Redirects',
633
+ type: 'number',
634
+ value: redirects.length,
635
+ icon: 'lucide:cornerDownRight',
636
+ description: 'Matching derived redirects',
637
+ color: '#0ea5e9',
638
+ },
639
+ {
640
+ id: 'allDisabledRoutes',
641
+ title: 'Disabled',
642
+ type: 'number',
643
+ value: disabledCount,
644
+ icon: 'lucide:circlePause',
645
+ description: 'Disabled Standard and Special routes',
646
+ color: disabledCount > 0 ? '#ef4444' : '#6b7280',
647
+ },
648
+ ];
649
+ }
650
+
651
+ if (this.activeSection === 'redirects') {
652
+ const activeCount = redirects.filter((redirect) => redirect.status === 'active').length;
653
+ const coveredCount = redirects.filter((redirect) => redirect.status === 'covered').length;
654
+ const skippedCount = redirects.filter((redirect) => redirect.status === 'skipped').length;
655
+ return [
656
+ {
657
+ id: 'matchingRedirects',
658
+ title: 'Matching',
659
+ type: 'number',
660
+ value: redirects.length,
661
+ icon: 'lucide:cornerDownRight',
662
+ description: 'Redirects in this owner scope',
663
+ color: '#3b82f6',
664
+ },
665
+ {
666
+ id: 'activeRedirects',
667
+ title: 'Active',
668
+ type: 'number',
669
+ value: activeCount,
670
+ icon: 'lucide:circleCheck',
671
+ description: 'Generated at runtime',
672
+ color: '#22c55e',
673
+ },
674
+ {
675
+ id: 'coveredRedirects',
676
+ title: 'Covered',
677
+ type: 'number',
678
+ value: coveredCount,
679
+ icon: 'lucide:shieldCheck',
680
+ description: 'Handled by explicit HTTP routes',
681
+ color: '#8b5cf6',
682
+ },
683
+ {
684
+ id: 'skippedRedirects',
685
+ title: 'Skipped',
686
+ type: 'number',
687
+ value: skippedCount,
688
+ icon: 'lucide:triangleAlert',
689
+ description: 'Overlaps explicit HTTP routes',
690
+ color: skippedCount > 0 ? '#f59e0b' : '#6b7280',
691
+ },
692
+ ];
693
+ }
694
+
695
+ const routes = this.activeSection === 'special' ? specialRoutes : standardRoutes;
696
+ const enabledCount = routes.filter((route) => route.enabled).length;
697
+ const disabledCount = routes.length - enabledCount;
698
+ const readOnlyCount = routes.filter((route) => getRouteOwner(route) !== 'operator').length;
699
+ const scopeName = this.activeSection === 'special' ? 'Special' : 'Standard';
700
+ return [
701
+ {
702
+ id: `matching${scopeName}Routes`,
703
+ title: 'Matching',
704
+ type: 'number',
705
+ value: routes.length,
706
+ icon: 'lucide:route',
707
+ description: `${scopeName} routes in this owner scope`,
708
+ color: '#3b82f6',
709
+ },
710
+ {
711
+ id: `enabled${scopeName}Routes`,
712
+ title: 'Enabled',
713
+ type: 'number',
714
+ value: enabledCount,
715
+ icon: 'lucide:circleCheck',
716
+ description: 'Currently active',
717
+ color: '#22c55e',
718
+ },
719
+ {
720
+ id: `disabled${scopeName}Routes`,
721
+ title: 'Disabled',
722
+ type: 'number',
723
+ value: disabledCount,
724
+ icon: 'lucide:circlePause',
725
+ description: 'Configured but inactive',
726
+ color: disabledCount > 0 ? '#f59e0b' : '#6b7280',
727
+ },
728
+ {
729
+ id: `readOnly${scopeName}Routes`,
730
+ title: 'Read-only',
731
+ type: 'number',
732
+ value: readOnlyCount,
733
+ icon: 'lucide:lockKeyhole',
734
+ description: 'Managed by gateway clients or system',
735
+ color: '#8b5cf6',
736
+ },
737
+ ];
738
+ }
739
+
740
+ private getGridActions() {
741
+ const canCreateUserRoutes = this.ownerFilter === 'all' || this.ownerFilter === 'operator';
742
+ return [
743
+ ...(canCreateUserRoutes && (this.activeSection === 'standard' || this.activeSection === 'all')
744
+ ? [{
745
+ name: 'Add User Route',
746
+ iconName: 'lucide:plus',
747
+ action: () => this.showCreateRouteDialog(),
748
+ }]
749
+ : []),
750
+ ...(canCreateUserRoutes && (this.activeSection === 'special' || this.activeSection === 'all')
751
+ ? [{
752
+ name: 'Add User Special Forward',
753
+ iconName: 'lucide:wandSparkles',
754
+ action: () => showLetsEncryptHttp01ForwardDialog({
755
+ targets: this.profilesTargetsState.targets,
756
+ }),
757
+ }]
758
+ : []),
759
+ {
760
+ name: 'Refresh',
761
+ iconName: 'lucide:refreshCw',
762
+ action: () => this.refreshData(),
763
+ },
764
+ ];
765
+ }
766
+
633
767
  private async handleRouteClick(e: CustomEvent) {
634
768
  const clickedRoute = e.detail;
635
769
  if (!clickedRoute) return;
@@ -967,7 +1101,7 @@ export class OpsViewRoutes extends DeesElement {
967
1101
  ];
968
1102
 
969
1103
  const createModal = await DeesModal.createAndShow({
970
- heading: 'Add Route',
1104
+ heading: 'Add User Route',
971
1105
  content: html`
972
1106
  <dees-form>
973
1107
  <dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
@@ -1117,8 +1251,15 @@ export class OpsViewRoutes extends DeesElement {
1117
1251
  }
1118
1252
  }
1119
1253
 
1120
- private refreshData() {
1121
- appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
1254
+ private async refreshData(): Promise<void> {
1255
+ await Promise.all([
1256
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null),
1257
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchHttpRedirectsAction, null),
1258
+ appstate.profilesTargetsStatePart.dispatchAction(
1259
+ appstate.fetchProfilesAndTargetsAction,
1260
+ null,
1261
+ ),
1262
+ ]);
1122
1263
  }
1123
1264
 
1124
1265
  private getSelectedRouteTypeOption(): string {
@@ -1279,6 +1420,6 @@ export class OpsViewRoutes extends DeesElement {
1279
1420
  }));
1280
1421
  }
1281
1422
 
1282
- await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
1423
+ await this.refreshData();
1283
1424
  }
1284
1425
  }