feat(webhook): add webhook endpoint and client push notifications, auto-refresh UI, and gitea id mapping fixes

This commit is contained in:
2026-02-24 19:41:52 +00:00
parent b576056fa1
commit 71176a1856
24 changed files with 354 additions and 166557 deletions

View File

@@ -43,6 +43,14 @@ export class GitopsDashboard extends DeesElement {
private resolvedViewTabs: Array<{ name: string; iconName: string; element: any }> = [];
// Auto-refresh timer
private autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
// WebSocket client
private ws: WebSocket | null = null;
private wsReconnectTimer: ReturnType<typeof setTimeout> | null = null;
private wsIntentionalClose = false;
constructor() {
super();
document.title = 'GitOps';
@@ -53,7 +61,11 @@ export class GitopsDashboard extends DeesElement {
this.loginState = loginState;
if (loginState.isLoggedIn) {
appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
this.connectWebSocket();
} else {
this.disconnectWebSocket();
}
this.manageAutoRefreshTimer();
});
this.rxSubscriptions.push(loginSubscription);
@@ -62,6 +74,7 @@ export class GitopsDashboard extends DeesElement {
.subscribe((uiState) => {
this.uiState = uiState;
this.syncAppdashView(uiState.activeView);
this.manageAutoRefreshTimer();
});
this.rxSubscriptions.push(uiSubscription);
}
@@ -78,6 +91,36 @@ export class GitopsDashboard extends DeesElement {
width: 100%;
height: 100vh;
}
.auto-refresh-toggle {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1000;
background: rgba(30, 30, 50, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 14px;
color: #ccc;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: blur(8px);
transition: background 0.2s;
}
.auto-refresh-toggle:hover {
background: rgba(40, 40, 70, 0.95);
}
.auto-refresh-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
}
.auto-refresh-dot.active {
background: #00ff88;
}
`,
];
@@ -92,6 +135,15 @@ export class GitopsDashboard extends DeesElement {
</dees-simple-appdash>
</dees-simple-login>
</div>
${this.loginState.isLoggedIn ? html`
<div
class="auto-refresh-toggle"
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
>
<span class="auto-refresh-dot ${this.uiState.autoRefresh ? 'active' : ''}"></span>
Auto-Refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
</div>
` : ''}
`;
}
@@ -160,6 +212,93 @@ export class GitopsDashboard extends DeesElement {
}
}
public override disconnectedCallback() {
super.disconnectedCallback();
this.clearAutoRefreshTimer();
this.disconnectWebSocket();
}
// ============================================================================
// Auto-refresh timer management
// ============================================================================
private manageAutoRefreshTimer(): void {
this.clearAutoRefreshTimer();
const { autoRefresh, refreshInterval } = this.uiState;
if (autoRefresh && this.loginState.isLoggedIn) {
this.autoRefreshTimer = setInterval(() => {
document.dispatchEvent(new CustomEvent('gitops-auto-refresh'));
}, refreshInterval);
}
}
private clearAutoRefreshTimer(): void {
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
this.autoRefreshTimer = null;
}
}
// ============================================================================
// WebSocket client for webhook push notifications
// ============================================================================
private connectWebSocket(): void {
if (this.ws) return;
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}`;
try {
this.wsIntentionalClose = false;
this.ws = new WebSocket(wsUrl);
this.ws.addEventListener('message', (event) => {
try {
const data = JSON.parse(event.data);
// TypedSocket wraps messages; look for webhookNotification method
if (data?.method === 'webhookNotification' || data?.type === 'webhookEvent') {
console.log('Webhook event received:', data);
document.dispatchEvent(new CustomEvent('gitops-auto-refresh'));
}
} catch {
// Not JSON, ignore
}
});
this.ws.addEventListener('close', () => {
this.ws = null;
if (!this.wsIntentionalClose && this.loginState.isLoggedIn) {
this.wsReconnectTimer = setTimeout(() => {
this.connectWebSocket();
}, 5000);
}
});
this.ws.addEventListener('error', () => {
// Will trigger close event
});
} catch (err) {
console.warn('WebSocket connection failed:', err);
}
}
private disconnectWebSocket(): void {
this.wsIntentionalClose = true;
if (this.wsReconnectTimer) {
clearTimeout(this.wsReconnectTimer);
this.wsReconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// ============================================================================
// Login
// ============================================================================
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;

View File

@@ -38,6 +38,8 @@ export class GitopsViewBuildlog extends DeesElement {
@state()
accessor selectedJobId: string = '';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -49,6 +51,18 @@ export class GitopsViewBuildlog extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.fetchLog();
}
public static styles = [

View File

@@ -19,12 +19,26 @@ export class GitopsViewConnections extends DeesElement {
activeConnectionId: null,
};
private _autoRefreshHandler: () => void;
constructor() {
super();
const sub = appstate.connectionsStatePart
.select((s) => s)
.subscribe((s) => { this.connectionsState = s; });
this.rxSubscriptions.push(sub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.refresh();
}
public static styles = [

View File

@@ -32,6 +32,8 @@ export class GitopsViewGroups extends DeesElement {
@state()
accessor selectedConnectionId: string = '';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -43,6 +45,18 @@ export class GitopsViewGroups extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.loadGroups();
}
public static styles = [

View File

@@ -30,6 +30,8 @@ export class GitopsViewOverview extends DeesElement {
currentJobLog: '',
};
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -41,6 +43,18 @@ export class GitopsViewOverview extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
appstate.connectionsStatePart.dispatchAction(appstate.fetchConnectionsAction, null);
}
public static styles = [

View File

@@ -35,6 +35,8 @@ export class GitopsViewPipelines extends DeesElement {
@state()
accessor selectedProjectId: string = '';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -46,6 +48,18 @@ export class GitopsViewPipelines extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.loadPipelines();
}
public static styles = [

View File

@@ -32,6 +32,8 @@ export class GitopsViewProjects extends DeesElement {
@state()
accessor selectedConnectionId: string = '';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -43,6 +45,18 @@ export class GitopsViewProjects extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.loadProjects();
}
public static styles = [

View File

@@ -38,6 +38,8 @@ export class GitopsViewSecrets extends DeesElement {
@state()
accessor selectedScopeId: string = '';
private _autoRefreshHandler: () => void;
constructor() {
super();
const connSub = appstate.connectionsStatePart
@@ -49,6 +51,18 @@ export class GitopsViewSecrets extends DeesElement {
.select((s) => s)
.subscribe((s) => { this.dataState = s; });
this.rxSubscriptions.push(dataSub);
this._autoRefreshHandler = () => this.handleAutoRefresh();
document.addEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
public override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('gitops-auto-refresh', this._autoRefreshHandler);
}
private handleAutoRefresh(): void {
this.loadSecrets();
}
public static styles = [