Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cd5db2d81 | |||
| de0b7d1fe0 | |||
| 4e32745a8f | |||
| 121573de2f | |||
| cd957526e2 | |||
| 7aa5f07731 | |||
| 5b6f7b30c3 | |||
| 18cc21a49e |
31
changelog.md
31
changelog.md
@@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.3 - fix(dcrouter)
|
||||||
|
persist proxy certificate validity dates and improve certificate status initialization
|
||||||
|
|
||||||
|
- Bump @push.rocks/smartacme dependency from ^9.0.0 to ^9.1.3
|
||||||
|
- Store validFrom and validUntil alongside proxy cert entries (/proxy-certs) when saving, extracting values by parsing PEM where possible
|
||||||
|
- Use stored cert entries (domain, publicKey, validUntil, validFrom) to populate certificateStatusMap at startup
|
||||||
|
- Fallback to SmartAcme /certs/ metadata and finally to parsing X.509 from stored PEM to determine expiry/issuedAt when initializing status
|
||||||
|
- Update opsserver certificate handler to parse publicKey PEM from cert-store and set expiry/issuedAt and issuer accordingly
|
||||||
|
- Adjust variable names and logging to reflect stored cert entry usage
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.2 - fix(certs)
|
||||||
|
Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler
|
||||||
|
|
||||||
|
- Track domains loaded from storageManager '/proxy-certs/' and populate certificateStatusMap with status, routeNames, expiryDate and issuedAt (when available) after SmartProxy starts
|
||||||
|
- Opsserver certificate handler now falls back to '/proxy-certs/{domain}' if '/certs/{cleanDomain}' is missing and marks cert-store-only entries as valid with issuer 'cert-store'
|
||||||
|
- Bump @push.rocks/smartproxy dependency from ^25.3.1 to ^25.4.0
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.1 - fix(smartacme,storage)
|
||||||
|
Respect wildcard domain requests when retrieving certificates and treat empty/whitespace storage values as null in getJSON
|
||||||
|
|
||||||
|
- Pass includeWildcard flag to smartAcme.getCertificateForDomain to avoid incorrectly including/excluding wildcard certificates based on whether the requested domain itself is a wildcard
|
||||||
|
- Detect wildcard domains via domain.startsWith('*.') and set includeWildcard to false for wildcard requests
|
||||||
|
- Treat empty or whitespace-only stored values as null in StorageManager.getJSON to avoid parsing empty strings as JSON and potential errors
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.0 - feat(ts_web)
|
||||||
|
add Certificate Management documentation and ops-view-certificates reference
|
||||||
|
|
||||||
|
- Adds a new 'Certificate Management' section to ts_web/readme.md describing domain-centric overview, certificate sources (ACME/provision/static), expiry monitoring, per-domain backoff, and one-click reprovisioning
|
||||||
|
- Adds ops-view-certificates.ts entry to the ops UI file list
|
||||||
|
- Documents new route mapping '/certificates' in the readme navigation
|
||||||
|
|
||||||
## 2026-02-16 - 6.1.0 - feat(certs)
|
## 2026-02-16 - 6.1.0 - feat(certs)
|
||||||
integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler
|
integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "6.1.0",
|
"version": "6.2.3",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^9.0.0",
|
"@push.rocks/smartacme": "^9.1.3",
|
||||||
"@push.rocks/smartdata": "^7.0.15",
|
"@push.rocks/smartdata": "^7.0.15",
|
||||||
"@push.rocks/smartdns": "^7.8.1",
|
"@push.rocks/smartdns": "^7.8.1",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.3.1",
|
"@push.rocks/smartproxy": "^25.4.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
|||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
'@push.rocks/smartacme':
|
'@push.rocks/smartacme':
|
||||||
specifier: ^9.0.0
|
specifier: ^9.1.3
|
||||||
version: 9.1.2(socks@2.8.7)
|
version: 9.1.3(socks@2.8.7)
|
||||||
'@push.rocks/smartdata':
|
'@push.rocks/smartdata':
|
||||||
specifier: ^7.0.15
|
specifier: ^7.0.15
|
||||||
version: 7.0.15(socks@2.8.7)
|
version: 7.0.15(socks@2.8.7)
|
||||||
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^25.3.1
|
specifier: ^25.4.0
|
||||||
version: 25.3.1
|
version: 25.4.0
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -852,8 +852,8 @@ packages:
|
|||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||||
|
|
||||||
'@push.rocks/smartacme@9.1.2':
|
'@push.rocks/smartacme@9.1.3':
|
||||||
resolution: {integrity: sha512-pcYJ9iFwCV4KcRRrxU8VJBYTjgzVv1LnWqkFcEDJJvLdnxwxggpwMZZ+g/CCJlb7gOUkDuTPbfCX7deDvWeIoQ==}
|
resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==}
|
||||||
|
|
||||||
'@push.rocks/smartarchive@4.2.4':
|
'@push.rocks/smartarchive@4.2.4':
|
||||||
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
||||||
@@ -1031,8 +1031,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.3.1':
|
'@push.rocks/smartproxy@25.4.0':
|
||||||
resolution: {integrity: sha512-kGJGpx3KBUz+qWU2L9B2gbZoUbQEG2BFe6ZzK0b68Y32nHoSIMjol14hzc3sRgW1p/loWy+Gj+5j0KuVytKWmA==}
|
resolution: {integrity: sha512-aU7ySk/2llRs6hcIGrl4gjuXsJOmLuVv952ys4H1yZyZSdPx7G8m5gJl6RxB5Rp8GzM2YaKUvCp00apcGcnEfw==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -5782,7 +5782,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartacme@9.1.2(socks@2.8.7)':
|
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@apiclient.xyz/cloudflare': 7.1.0
|
'@apiclient.xyz/cloudflare': 7.1.0
|
||||||
'@peculiar/x509': 1.14.3
|
'@peculiar/x509': 1.14.3
|
||||||
@@ -6369,7 +6369,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.3.1':
|
'@push.rocks/smartproxy@25.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.1.11
|
'@push.rocks/smartlog': 3.1.11
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.1.0',
|
version: '6.2.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,6 +445,9 @@ export class DcRouter {
|
|||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
console.log('Setting up SmartProxy with combined configuration');
|
console.log('Setting up SmartProxy with combined configuration');
|
||||||
|
|
||||||
|
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||||
|
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||||
|
|
||||||
// Create SmartProxy configuration
|
// Create SmartProxy configuration
|
||||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||||
...this.options.smartProxyConfig,
|
...this.options.smartProxyConfig,
|
||||||
@@ -456,13 +459,23 @@ export class DcRouter {
|
|||||||
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
|
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const data = await this.storageManager.getJSON(key);
|
const data = await this.storageManager.getJSON(key);
|
||||||
if (data) certs.push(data);
|
if (data) {
|
||||||
|
certs.push(data);
|
||||||
|
loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return certs;
|
return certs;
|
||||||
},
|
},
|
||||||
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
|
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
|
||||||
|
let validUntil: number | undefined;
|
||||||
|
let validFrom: number | undefined;
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(publicKey);
|
||||||
|
validUntil = new Date(x509.validTo).getTime();
|
||||||
|
validFrom = new Date(x509.validFrom).getTime();
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
|
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
|
||||||
domain, publicKey, privateKey, ca,
|
domain, publicKey, privateKey, ca, validUntil, validFrom,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
remove: async (domain: string) => {
|
remove: async (domain: string) => {
|
||||||
@@ -499,7 +512,10 @@ export class DcRouter {
|
|||||||
// smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
|
// smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
|
||||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||||
eventComms.setSource('smartacme-dns-01');
|
eventComms.setSource('smartacme-dns-01');
|
||||||
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
const isWildcardDomain = domain.startsWith('*.');
|
||||||
|
const cert = await this.smartAcme.getCertificateForDomain(domain, {
|
||||||
|
includeWildcard: !isWildcardDomain,
|
||||||
|
});
|
||||||
if (cert.validUntil) {
|
if (cert.validUntil) {
|
||||||
eventComms.setExpiryDate(new Date(cert.validUntil));
|
eventComms.setExpiryDate(new Date(cert.validUntil));
|
||||||
}
|
}
|
||||||
@@ -577,6 +593,59 @@ export class DcRouter {
|
|||||||
await this.smartProxy.start();
|
await this.smartProxy.start();
|
||||||
console.log('[DcRouter] SmartProxy started successfully');
|
console.log('[DcRouter] SmartProxy started successfully');
|
||||||
|
|
||||||
|
// Populate certificateStatusMap for certs loaded from store at startup
|
||||||
|
for (const entry of loadedCertEntries) {
|
||||||
|
if (!this.certificateStatusMap.has(entry.domain)) {
|
||||||
|
const routeNames = this.findRouteNamesForDomain(entry.domain);
|
||||||
|
let expiryDate: string | undefined;
|
||||||
|
let issuedAt: string | undefined;
|
||||||
|
|
||||||
|
// Use validUntil/validFrom from stored proxy-certs data if available
|
||||||
|
if (entry.validUntil) {
|
||||||
|
expiryDate = new Date(entry.validUntil).toISOString();
|
||||||
|
}
|
||||||
|
if (entry.validFrom) {
|
||||||
|
issuedAt = new Date(entry.validFrom).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try SmartAcme /certs/ metadata as secondary source
|
||||||
|
if (!expiryDate) {
|
||||||
|
try {
|
||||||
|
const cleanDomain = entry.domain.replace(/^\*\.?/, '');
|
||||||
|
const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (certMeta?.validUntil) {
|
||||||
|
expiryDate = new Date(certMeta.validUntil).toISOString();
|
||||||
|
}
|
||||||
|
if (certMeta?.created && !issuedAt) {
|
||||||
|
issuedAt = new Date(certMeta.created).toISOString();
|
||||||
|
}
|
||||||
|
} catch { /* no metadata available */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: parse X509 from PEM to get expiry
|
||||||
|
if (!expiryDate && entry.publicKey) {
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(entry.publicKey);
|
||||||
|
expiryDate = new Date(x509.validTo).toISOString();
|
||||||
|
if (!issuedAt) {
|
||||||
|
issuedAt = new Date(x509.validFrom).toISOString();
|
||||||
|
}
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.certificateStatusMap.set(entry.domain, {
|
||||||
|
status: 'valid',
|
||||||
|
routeNames,
|
||||||
|
expiryDate,
|
||||||
|
issuedAt,
|
||||||
|
source: 'cert-store',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loadedCertEntries.length > 0) {
|
||||||
|
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`SmartProxy started with ${routes.length} routes`);
|
console.log(`SmartProxy started with ${routes.length} routes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,13 +156,29 @@ export class CertificateHandler {
|
|||||||
// Check persisted cert data from StorageManager
|
// Check persisted cert data from StorageManager
|
||||||
if (status === 'unknown') {
|
if (status === 'unknown') {
|
||||||
const cleanDomain = domain.replace(/^\*\.?/, '');
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||||
const certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
let certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (!certData) {
|
||||||
|
// Also check certStore path (proxy-certs)
|
||||||
|
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${domain}`);
|
||||||
|
}
|
||||||
if (certData?.validUntil) {
|
if (certData?.validUntil) {
|
||||||
expiryDate = new Date(certData.validUntil).toISOString();
|
expiryDate = new Date(certData.validUntil).toISOString();
|
||||||
if (certData.created) {
|
if (certData.created) {
|
||||||
issuedAt = new Date(certData.created).toISOString();
|
issuedAt = new Date(certData.created).toISOString();
|
||||||
}
|
}
|
||||||
issuer = 'smartacme-dns-01';
|
issuer = 'smartacme-dns-01';
|
||||||
|
} else if (certData?.publicKey) {
|
||||||
|
// certStore has the cert — parse PEM for expiry
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(certData.publicKey);
|
||||||
|
expiryDate = new Date(x509.validTo).toISOString();
|
||||||
|
issuedAt = new Date(x509.validFrom).toISOString();
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
|
status = 'valid';
|
||||||
|
issuer = 'cert-store';
|
||||||
|
} else if (certData) {
|
||||||
|
status = 'valid';
|
||||||
|
issuer = 'cert-store';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export class StorageManager {
|
|||||||
*/
|
*/
|
||||||
async getJSON<T = any>(key: string): Promise<T | null> {
|
async getJSON<T = any>(key: string): Promise<T | null> {
|
||||||
const value = await this.get(key);
|
const value = await this.get(key);
|
||||||
if (value === null) {
|
if (value === null || value.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '6.1.0',
|
version: '6.2.3',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Security** — Security incidents from email processing
|
- **Security** — Security incidents from email processing
|
||||||
- Bounce record management and suppression list controls
|
- Bounce record management and suppression list controls
|
||||||
|
|
||||||
|
### 🔐 Certificate Management
|
||||||
|
- Domain-centric certificate overview with status indicators
|
||||||
|
- Certificate source tracking (ACME, provision function, static)
|
||||||
|
- Expiry date monitoring and alerts
|
||||||
|
- Per-domain backoff status for failed provisions
|
||||||
|
- One-click reprovisioning per domain
|
||||||
|
|
||||||
### 📜 Log Viewer
|
### 📜 Log Viewer
|
||||||
- Real-time log streaming
|
- Real-time log streaming
|
||||||
- Filter by log level (error, warning, info, debug)
|
- Filter by log level (error, warning, info, debug)
|
||||||
@@ -77,6 +84,7 @@ ts_web/
|
|||||||
├── ops-view-overview.ts # Overview statistics
|
├── ops-view-overview.ts # Overview statistics
|
||||||
├── ops-view-network.ts # Network monitoring
|
├── ops-view-network.ts # Network monitoring
|
||||||
├── ops-view-emails.ts # Email queue management
|
├── ops-view-emails.ts # Email queue management
|
||||||
|
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||||
├── ops-view-logs.ts # Log viewer
|
├── ops-view-logs.ts # Log viewer
|
||||||
├── ops-view-config.ts # Configuration display
|
├── ops-view-config.ts # Configuration display
|
||||||
├── ops-view-security.ts # Security dashboard
|
├── ops-view-security.ts # Security dashboard
|
||||||
@@ -132,6 +140,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
|
|||||||
/emails/sent → Sent emails
|
/emails/sent → Sent emails
|
||||||
/emails/failed → Failed emails
|
/emails/failed → Failed emails
|
||||||
/emails/security → Security incidents
|
/emails/security → Security incidents
|
||||||
|
/certificates → Certificate management
|
||||||
/logs → Log viewer
|
/logs → Log viewer
|
||||||
/configuration → System configuration
|
/configuration → System configuration
|
||||||
/security → Security dashboard
|
/security → Security dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user