@serve.zone/dcrouter 6.7.0 → 6.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.
@@ -241,6 +241,61 @@ export class OpsViewCertificates extends DeesElement {
241
241
  : '',
242
242
  })}
243
243
  .dataActions=${[
244
+ {
245
+ name: 'Import Certificate',
246
+ iconName: 'lucide:upload',
247
+ type: ['header'],
248
+ actionFunc: async () => {
249
+ const { DeesModal } = await import('@design.estate/dees-catalog');
250
+ await DeesModal.createAndShow({
251
+ heading: 'Import Certificate',
252
+ content: html`
253
+ <dees-form>
254
+ <dees-input-fileupload
255
+ key="certJsonFile"
256
+ label="Certificate JSON (.tsclass.cert.json)"
257
+ accept=".json"
258
+ .multiple=${false}
259
+ required
260
+ ></dees-input-fileupload>
261
+ </dees-form>
262
+ `,
263
+ menuOptions: [
264
+ {
265
+ name: 'Import',
266
+ iconName: 'lucide:upload',
267
+ action: async (modal) => {
268
+ const { DeesToast } = await import('@design.estate/dees-catalog');
269
+ try {
270
+ const form = modal.shadowRoot.querySelector('dees-form') as any;
271
+ const formData = await form.collectFormData();
272
+ const files = formData.certJsonFile;
273
+ if (!files || files.length === 0) {
274
+ DeesToast.show({ message: 'Please select a JSON file.', type: 'warning', duration: 3000 });
275
+ return;
276
+ }
277
+ const file = files[0];
278
+ const text = await file.text();
279
+ const cert = JSON.parse(text);
280
+ if (!cert.domainName || !cert.publicKey || !cert.privateKey) {
281
+ DeesToast.show({ message: 'Invalid cert JSON: missing domainName, publicKey, or privateKey.', type: 'error', duration: 4000 });
282
+ return;
283
+ }
284
+ await appstate.certificateStatePart.dispatchAction(
285
+ appstate.importCertificateAction,
286
+ cert,
287
+ );
288
+ DeesToast.show({ message: `Certificate imported for ${cert.domainName}`, type: 'success', duration: 3000 });
289
+ modal.destroy();
290
+ } catch (err) {
291
+ DeesToast.show({ message: `Import failed: ${err.message}`, type: 'error', duration: 4000 });
292
+ }
293
+ },
294
+ },
295
+ ],
296
+ });
297
+ },
298
+ },
244
299
  {
245
300
  name: 'Reprovision',
246
301
  iconName: 'lucide:RefreshCw',
@@ -268,6 +323,63 @@ export class OpsViewCertificates extends DeesElement {
268
323
  });
269
324
  },
270
325
  },
326
+ {
327
+ name: 'Export',
328
+ iconName: 'lucide:download',
329
+ type: ['contextmenu'],
330
+ actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
331
+ const { DeesToast } = await import('@design.estate/dees-catalog');
332
+ const cert = actionData.item;
333
+ try {
334
+ const response = await appstate.fetchCertificateExport(cert.domain);
335
+ if (response.success && response.cert) {
336
+ const safeDomain = cert.domain.replace(/\*/g, '_wildcard');
337
+ this.downloadJsonFile(`${safeDomain}.tsclass.cert.json`, response.cert);
338
+ DeesToast.show({ message: `Certificate exported for ${cert.domain}`, type: 'success', duration: 3000 });
339
+ } else {
340
+ DeesToast.show({ message: response.message || 'Export failed', type: 'error', duration: 4000 });
341
+ }
342
+ } catch (err) {
343
+ DeesToast.show({ message: `Export failed: ${err.message}`, type: 'error', duration: 4000 });
344
+ }
345
+ },
346
+ },
347
+ {
348
+ name: 'Delete',
349
+ iconName: 'lucide:trash-2',
350
+ type: ['contextmenu'],
351
+ actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
352
+ const cert = actionData.item;
353
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
354
+ await DeesModal.createAndShow({
355
+ heading: `Delete Certificate: ${cert.domain}`,
356
+ content: html`
357
+ <div style="padding: 20px; font-size: 14px;">
358
+ <p>Are you sure you want to delete the certificate data for <strong>${cert.domain}</strong>?</p>
359
+ <p style="color: #f59e0b; margin-top: 12px;">Note: The certificate may remain in proxy memory until the next restart or reprovisioning.</p>
360
+ </div>
361
+ `,
362
+ menuOptions: [
363
+ {
364
+ name: 'Delete',
365
+ iconName: 'lucide:trash-2',
366
+ action: async (modal) => {
367
+ try {
368
+ await appstate.certificateStatePart.dispatchAction(
369
+ appstate.deleteCertificateAction,
370
+ cert.domain,
371
+ );
372
+ DeesToast.show({ message: `Certificate deleted for ${cert.domain}`, type: 'success', duration: 3000 });
373
+ modal.destroy();
374
+ } catch (err) {
375
+ DeesToast.show({ message: `Delete failed: ${err.message}`, type: 'error', duration: 4000 });
376
+ }
377
+ },
378
+ },
379
+ ],
380
+ });
381
+ },
382
+ },
271
383
  {
272
384
  name: 'View Details',
273
385
  iconName: 'lucide:Search',
@@ -309,6 +421,19 @@ export class OpsViewCertificates extends DeesElement {
309
421
  `;
310
422
  }
311
423
 
424
+ private downloadJsonFile(filename: string, data: any): void {
425
+ const json = JSON.stringify(data, null, 2);
426
+ const blob = new Blob([json], { type: 'application/json' });
427
+ const url = URL.createObjectURL(blob);
428
+ const a = document.createElement('a');
429
+ a.href = url;
430
+ a.download = filename;
431
+ document.body.appendChild(a);
432
+ a.click();
433
+ document.body.removeChild(a);
434
+ URL.revokeObjectURL(url);
435
+ }
436
+
312
437
  private renderRoutePills(routeNames: string[]): TemplateResult {
313
438
  const maxShow = 3;
314
439
  const visible = routeNames.slice(0, maxShow);
@@ -114,6 +114,17 @@ export class OpsViewRemoteIngress extends DeesElement {
114
114
  background: ${cssManager.bdTheme('#eff6ff', '#172554')};
115
115
  color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
116
116
  }
117
+
118
+ .portBadge.manual {
119
+ background: ${cssManager.bdTheme('#eff6ff', '#172554')};
120
+ color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
121
+ }
122
+
123
+ .portBadge.derived {
124
+ background: ${cssManager.bdTheme('#ecfdf5', '#022c22')};
125
+ color: ${cssManager.bdTheme('#047857', '#34d399')};
126
+ border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
127
+ }
117
128
  `,
118
129
  ];
119
130
 
@@ -203,7 +214,8 @@ export class OpsViewRemoteIngress extends DeesElement {
203
214
  content: html`
204
215
  <dees-form>
205
216
  <dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
206
- <dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated, auto-derived if empty)'}></dees-input-text>
217
+ <dees-input-text .key=${'listenPorts'} .label=${'Additional Manual Ports (comma-separated, optional)'}></dees-input-text>
218
+ <dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${true}></dees-input-checkbox>
207
219
  <dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
208
220
  </dees-form>
209
221
  `,
@@ -226,12 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement {
226
238
  const listenPorts = portsStr
227
239
  ? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
228
240
  : undefined;
241
+ const autoDerivePorts = formData.autoDerivePorts !== false;
229
242
  const tags = formData.tags
230
243
  ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
231
244
  : undefined;
232
245
  await appstate.remoteIngressStatePart.dispatchAction(
233
246
  appstate.createRemoteIngressAction,
234
- { name, listenPorts, tags },
247
+ { name, listenPorts, autoDerivePorts, tags },
235
248
  );
236
249
  await modalArg.destroy();
237
250
  },
@@ -266,6 +279,61 @@ export class OpsViewRemoteIngress extends DeesElement {
266
279
  );
267
280
  },
268
281
  },
282
+ {
283
+ name: 'Edit',
284
+ iconName: 'lucide:pencil',
285
+ type: ['inRow', 'contextmenu'] as any,
286
+ actionFunc: async (actionData: any) => {
287
+ const edge = actionData.item as interfaces.data.IRemoteIngress;
288
+ const { DeesModal } = await import('@design.estate/dees-catalog');
289
+ await DeesModal.createAndShow({
290
+ heading: `Edit Edge: ${edge.name}`,
291
+ content: html`
292
+ <dees-form>
293
+ <dees-input-text .key=${'name'} .label=${'Name'} .value=${edge.name}></dees-input-text>
294
+ <dees-input-text .key=${'listenPorts'} .label=${'Manual Ports (comma-separated)'} .value=${(edge.listenPorts || []).join(', ')}></dees-input-text>
295
+ <dees-input-checkbox .key=${'autoDerivePorts'} .label=${'Auto-derive ports from routes'} .value=${edge.autoDerivePorts !== false}></dees-input-checkbox>
296
+ <dees-input-text .key=${'tags'} .label=${'Tags (comma-separated)'} .value=${(edge.tags || []).join(', ')}></dees-input-text>
297
+ </dees-form>
298
+ `,
299
+ menuOptions: [
300
+ {
301
+ name: 'Cancel',
302
+ iconName: 'lucide:x',
303
+ action: async (modalArg: any) => await modalArg.destroy(),
304
+ },
305
+ {
306
+ name: 'Save',
307
+ iconName: 'lucide:check',
308
+ action: async (modalArg: any) => {
309
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
310
+ if (!form) return;
311
+ const formData = await form.collectFormData();
312
+ const portsStr = formData.listenPorts?.trim();
313
+ const listenPorts = portsStr
314
+ ? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
315
+ : [];
316
+ const autoDerivePorts = formData.autoDerivePorts !== false;
317
+ const tags = formData.tags
318
+ ? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
319
+ : [];
320
+ await appstate.remoteIngressStatePart.dispatchAction(
321
+ appstate.updateRemoteIngressAction,
322
+ {
323
+ id: edge.id,
324
+ name: formData.name || edge.name,
325
+ listenPorts,
326
+ autoDerivePorts,
327
+ tags,
328
+ },
329
+ );
330
+ await modalArg.destroy();
331
+ },
332
+ },
333
+ ],
334
+ });
335
+ },
336
+ },
269
337
  {
270
338
  name: 'Regenerate Secret',
271
339
  iconName: 'lucide:key',
@@ -317,13 +385,12 @@ export class OpsViewRemoteIngress extends DeesElement {
317
385
  }
318
386
 
319
387
  private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
320
- const hasManualPorts = edge.listenPorts && edge.listenPorts.length > 0;
321
- const ports = hasManualPorts ? edge.listenPorts : (edge.effectiveListenPorts || []);
322
- const isAuto = !hasManualPorts && ports.length > 0;
323
- if (ports.length === 0) {
388
+ const manualPorts = edge.manualPorts || [];
389
+ const derivedPorts = edge.derivedPorts || [];
390
+ if (manualPorts.length === 0 && derivedPorts.length === 0) {
324
391
  return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
325
392
  }
326
- return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}${isAuto ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
393
+ return html`<div class="portsDisplay">${manualPorts.map(p => html`<span class="portBadge manual">${p}</span>`)}${derivedPorts.map(p => html`<span class="portBadge derived">${p}</span>`)}${derivedPorts.length > 0 ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
327
394
  }
328
395
 
329
396
  private getEdgeTunnelCount(edgeId: string): number {