fix(typescript): tighten TypeScript null safety and error handling across backend and ops UI

This commit is contained in:
2026-03-26 07:40:56 +00:00
parent 0195a21f30
commit 44f2a7f0a9
40 changed files with 414 additions and 451 deletions

View File

@@ -195,17 +195,18 @@ export class OpsDashboard extends DeesElement {
}
public async firstUpdated() {
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
simpleLogin.addEventListener('login', (e: CustomEvent) => {
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
simpleLogin.addEventListener('login', (e: Event) => {
// Handle logout event
this.login(e.detail.data.username, e.detail.data.password);
const detail = (e as CustomEvent).detail;
this.login(detail.data.username, detail.data.password);
});
// Handle view changes
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => {
const viewName = e.detail.view.name.toLowerCase();
appDash.addEventListener('view-select', (e: Event) => {
const viewName = (e as CustomEvent).detail.view.name.toLowerCase();
// Use router for navigation instead of direct state update
appRouter.navigateToView(viewName);
});
@@ -217,7 +218,7 @@ export class OpsDashboard extends DeesElement {
}
// Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState();
const loginState = appstate.loginStatePart.getState()!;
if (loginState.identity?.jwt) {
if (loginState.identity.expiresAt > Date.now()) {
// Client-side expiry looks valid — verify with server (keypair may have changed)
@@ -229,7 +230,7 @@ export class OpsDashboard extends DeesElement {
if (response.valid) {
// JWT confirmed valid by server
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await (simpleLogin as any).switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
@@ -250,8 +251,8 @@ export class OpsDashboard extends DeesElement {
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
console.log(`Attempting to login...`);
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
const form = simpleLogin.shadowRoot.querySelector('dees-form');
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
form.setStatus('pending', 'Logging in...');
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
@@ -262,14 +263,14 @@ export class OpsDashboard extends DeesElement {
if (state.identity) {
console.log('Login successful');
this.loginState = state;
form.setStatus('success', 'Logged in!');
await simpleLogin.switchToSlottedContent();
form!.setStatus('success', 'Logged in!');
await simpleLogin!.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
form.setStatus('error', 'Login failed!');
form!.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000);
form.reset();
form!.reset();
}
}
}

View File

@@ -21,7 +21,7 @@ declare global {
@customElement('ops-view-certificates')
export class OpsViewCertificates extends DeesElement {
@state()
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState();
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState()!;
constructor() {
super();
@@ -264,10 +264,10 @@ export class OpsViewCertificates extends DeesElement {
{
name: 'Import',
iconName: 'lucide:upload',
action: async (modal) => {
action: async (modal: any) => {
const { DeesToast } = await import('@design.estate/dees-catalog');
try {
const form = modal.shadowRoot.querySelector('dees-form') as any;
const form = modal.shadowRoot!.querySelector('dees-form') as any;
const formData = await form.collectFormData();
const files = formData.certJsonFile;
if (!files || files.length === 0) {
@@ -287,8 +287,8 @@ export class OpsViewCertificates extends DeesElement {
);
DeesToast.show({ message: `Certificate imported for ${cert.domainName}`, type: 'success', duration: 3000 });
modal.destroy();
} catch (err) {
DeesToast.show({ message: `Import failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Import failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},
@@ -339,8 +339,8 @@ export class OpsViewCertificates extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Export failed', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Export failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Export failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},
@@ -363,7 +363,7 @@ export class OpsViewCertificates extends DeesElement {
{
name: 'Delete',
iconName: 'lucide:trash-2',
action: async (modal) => {
action: async (modal: any) => {
try {
await appstate.certificateStatePart.dispatchAction(
appstate.deleteCertificateAction,
@@ -371,8 +371,8 @@ export class OpsViewCertificates extends DeesElement {
);
DeesToast.show({ message: `Certificate deleted for ${cert.domain}`, type: 'success', duration: 3000 });
modal.destroy();
} catch (err) {
DeesToast.show({ message: `Delete failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Delete failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},

View File

@@ -102,7 +102,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
private renderSystemSection(sys: NonNullable<appstate.IConfigState['config']>['system']): TemplateResult {
// Annotate proxy IPs with source hint when Remote Ingress is active
const ri = this.configState.config?.remoteIngress;
let proxyIpValues: string[] | null = sys.proxyIps.length > 0 ? [...sys.proxyIps] : null;
@@ -133,7 +133,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
private renderSmartProxySection(proxy: NonNullable<appstate.IConfigState['config']>['smartProxy']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Route Count', value: proxy.routeCount },
];
@@ -164,7 +164,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
private renderEmailSection(email: NonNullable<appstate.IConfigState['config']>['email']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
{ key: 'Hostname', value: email.hostname },
@@ -196,7 +196,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
private renderDnsSection(dns: NonNullable<appstate.IConfigState['config']>['dns']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Port', value: dns.port },
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
@@ -216,7 +216,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
private renderTlsSection(tls: NonNullable<appstate.IConfigState['config']>['tls']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Contact Email', value: tls.contactEmail },
{ key: 'Domain', value: tls.domain },
@@ -242,7 +242,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
private renderCacheSection(cache: NonNullable<appstate.IConfigState['config']>['cache']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Storage Path', value: cache.storagePath },
{ key: 'DB Name', value: cache.dbName },
@@ -267,7 +267,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
private renderRadiusSection(radius: NonNullable<appstate.IConfigState['config']>['radius']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Auth Port', value: radius.authPort },
{ key: 'Accounting Port', value: radius.acctPort },
@@ -296,7 +296,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
private renderRemoteIngressSection(ri: NonNullable<appstate.IConfigState['config']>['remoteIngress']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Tunnel Port', value: ri.tunnelPort },
{ key: 'Hub Domain', value: ri.hubDomain },

View File

@@ -83,13 +83,13 @@ export class OpsViewEmails extends DeesElement {
private async handleEmailClick(e: CustomEvent<interfaces.requests.IEmail>) {
const emailSummary = e.detail;
try {
const context = appstate.loginStatePart.getState();
const context = appstate.loginStatePart.getState()!;
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetEmailDetail
>('/typedrequest', 'getEmailDetail');
const response = await request.fire({
identity: context.identity,
identity: context.identity!,
emailId: emailSummary.id,
});

View File

@@ -29,10 +29,10 @@ interface INetworkRequest {
@customElement('ops-view-network')
export class OpsViewNetwork extends DeesElement {
@state()
accessor statsState = appstate.statsStatePart.getState();
accessor statsState = appstate.statsStatePart.getState()!;
@state()
accessor networkState = appstate.networkStatePart.getState();
accessor networkState = appstate.networkStatePart.getState()!;
@state()

View File

@@ -21,7 +21,7 @@ declare global {
@customElement('ops-view-remoteingress')
export class OpsViewRemoteIngress extends DeesElement {
@state()
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState()!;
constructor() {
super();
@@ -184,7 +184,7 @@ export class OpsViewRemoteIngress extends DeesElement {
@click=${async () => {
const { DeesToast } = await import('@design.estate/dees-catalog');
try {
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId);
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId!);
if (response.success && response.token) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(response.token);
@@ -202,8 +202,8 @@ export class OpsViewRemoteIngress extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
}}
>Copy Connection Token</dees-button>
@@ -399,8 +399,8 @@ export class OpsViewRemoteIngress extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},