Files
gitops/ts_web/elements/views/actions/index.ts
Juergen Kunz e3f67d12a3 fix(core): fix secrets scan upserts, connection health checks, and frontend improvements
- Add upsert pattern to SecretsScanService to prevent duplicate key errors on repeated scans
- Auto-test connection health on startup so status reflects reality
- Fix Actions view to read identity from appstate instead of broken localStorage hack
- Fetch both project and group secrets in parallel, add "All Scopes" filter to Secrets view
- Enable noCache on UtilityWebsiteServer to prevent stale browser cache
2026-02-24 22:50:26 +00:00

210 lines
6.2 KiB
TypeScript

import * as plugins from '../../../plugins.js';
import * as interfaces from '../../../../ts_interfaces/index.js';
import * as appstate from '../../../appstate.js';
import { viewHostCss } from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('gitops-view-actions')
export class GitopsViewActions extends DeesElement {
@state()
accessor lastScanTimestamp: number = 0;
@state()
accessor isScanning: boolean = false;
@state()
accessor lastResult: {
connectionsScanned: number;
secretsFound: number;
errors: string[];
durationMs: number;
} | null = null;
@state()
accessor statusError: string = '';
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
.action-cards {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
max-width: 720px;
}
.action-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 28px;
}
.action-card-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.action-card-description {
font-size: 13px;
color: #999;
line-height: 1.5;
margin-bottom: 20px;
}
.info-grid {
display: grid;
grid-template-columns: 140px 1fr;
gap: 8px 16px;
margin-bottom: 20px;
font-size: 13px;
}
.info-label {
color: #888;
}
.info-value {
color: #ddd;
font-family: monospace;
}
.info-value.scanning {
color: #f0c040;
}
.info-value.error {
color: #ff6060;
}
.button-row {
display: flex;
gap: 12px;
align-items: center;
}
.errors-list {
margin-top: 12px;
max-height: 160px;
overflow-y: auto;
font-size: 12px;
color: #ff8080;
font-family: monospace;
line-height: 1.6;
background: rgba(255, 0, 0, 0.05);
border-radius: 6px;
padding: 8px 12px;
}
`,
];
public render(): TemplateResult {
const lastScanFormatted = this.lastScanTimestamp
? new Date(this.lastScanTimestamp).toLocaleString()
: 'Never';
return html`
<div class="view-title">Actions</div>
<div class="view-description">System actions and maintenance tasks</div>
<div class="action-cards">
<div class="action-card">
<div class="action-card-title">Secrets Cache Scan</div>
<div class="action-card-description">
Secrets are automatically scanned and cached every 24 hours.
Use "Force Full Scan" to trigger an immediate refresh of all secrets
across all connections, projects, and groups.
</div>
<div class="info-grid">
<div class="info-label">Status</div>
<div class="info-value ${this.isScanning ? 'scanning' : ''}">
${this.isScanning ? 'Scanning...' : 'Idle'}
</div>
<div class="info-label">Last Scan</div>
<div class="info-value">${lastScanFormatted}</div>
${this.lastResult ? html`
<div class="info-label">Connections</div>
<div class="info-value">${this.lastResult.connectionsScanned}</div>
<div class="info-label">Secrets Found</div>
<div class="info-value">${this.lastResult.secretsFound}</div>
<div class="info-label">Duration</div>
<div class="info-value">${(this.lastResult.durationMs / 1000).toFixed(1)}s</div>
${this.lastResult.errors.length > 0 ? html`
<div class="info-label">Errors</div>
<div class="info-value error">${this.lastResult.errors.length}</div>
` : ''}
` : ''}
</div>
${this.statusError ? html`
<div class="errors-list">${this.statusError}</div>
` : ''}
${this.lastResult?.errors?.length ? html`
<div class="errors-list">
${this.lastResult.errors.map((e) => html`<div>${e}</div>`)}
</div>
` : ''}
<div class="button-row">
<dees-button
.disabled=${this.isScanning}
@click=${() => this.forceScan()}
>Force Full Scan</dees-button>
<dees-button
@click=${() => this.refreshStatus()}
>Refresh Status</dees-button>
</div>
</div>
</div>
`;
}
async firstUpdated() {
await this.refreshStatus();
}
private getIdentity(): interfaces.data.IIdentity | null {
return appstate.loginStatePart.getState().identity;
}
private async refreshStatus(): Promise<void> {
const identity = this.getIdentity();
if (!identity) return;
try {
this.statusError = '';
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetScanStatus
>('/typedrequest', 'getScanStatus');
const response = await typedRequest.fire({ identity });
this.lastScanTimestamp = response.lastScanTimestamp;
this.isScanning = response.isScanning;
this.lastResult = response.lastResult;
} catch (err) {
this.statusError = `Failed to get status: ${err}`;
}
}
private async forceScan(): Promise<void> {
const identity = this.getIdentity();
if (!identity) return;
try {
this.statusError = '';
this.isScanning = true;
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_ForceScanSecrets
>('/typedrequest', 'forceScanSecrets');
const response = await typedRequest.fire({ identity });
this.lastResult = {
connectionsScanned: response.connectionsScanned,
secretsFound: response.secretsFound,
errors: response.errors,
durationMs: response.durationMs,
};
this.lastScanTimestamp = Date.now();
this.isScanning = false;
} catch (err) {
this.statusError = `Scan failed: ${err}`;
this.isScanning = false;
}
}
}