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

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/gitops',
version: '2.5.0',
version: '2.6.0',
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
}

View File

@@ -17,16 +17,23 @@ export class OpsServer {
public secretsHandler!: handlers.SecretsHandler;
public pipelinesHandler!: handlers.PipelinesHandler;
public logsHandler!: handlers.LogsHandler;
public webhookHandler!: handlers.WebhookHandler;
constructor(gitopsAppRef: GitopsApp) {
this.gitopsAppRef = gitopsAppRef;
}
public async start(port = 3000) {
// Create webhook handler before server so routes register via addCustomRoutes
this.webhookHandler = new handlers.WebhookHandler(this);
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
domain: 'localhost',
feedMetadata: undefined,
bundledContent: bundledFiles,
addCustomRoutes: async (typedserver) => {
this.webhookHandler.registerRoutes(typedserver);
},
});
// Chain typedrouters

View File

@@ -5,3 +5,4 @@ export { GroupsHandler } from './groups.handler.ts';
export { SecretsHandler } from './secrets.handler.ts';
export { PipelinesHandler } from './pipelines.handler.ts';
export { LogsHandler } from './logs.handler.ts';
export { WebhookHandler } from './webhook.handler.ts';

View File

@@ -0,0 +1,62 @@
import * as plugins from '../../plugins.ts';
import { logger } from '../../logging.ts';
import type { OpsServer } from '../classes.opsserver.ts';
import * as interfaces from '../../../ts_interfaces/index.ts';
export class WebhookHandler {
constructor(private opsServerRef: OpsServer) {}
public registerRoutes(typedserver: plugins.typedserver.TypedServer): void {
typedserver.addRoute('/webhook/:connectionId', 'POST', async (ctx) => {
const connectionId = ctx.params.connectionId;
// Validate connection exists
const connection = this.opsServerRef.gitopsAppRef.connectionManager.getConnection(connectionId);
if (!connection) {
return new Response(JSON.stringify({ error: 'Connection not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
// Parse event type from provider-specific headers
const giteaEvent = ctx.headers.get('X-Gitea-Event');
const gitlabEvent = ctx.headers.get('X-Gitlab-Event');
const event = giteaEvent || gitlabEvent || 'unknown';
const provider = giteaEvent ? 'gitea' : gitlabEvent ? 'gitlab' : 'unknown';
logger.info(`Webhook received: ${provider}/${event} for connection ${connection.name} (${connectionId})`);
// Broadcast to all connected frontends via TypedSocket
try {
const typedsocket = this.opsServerRef.server.typedserver.typedsocket;
if (typedsocket) {
const connections = await typedsocket.findAllTargetConnectionsByTag('allClients');
for (const conn of connections) {
const req = typedsocket.createTypedRequest<interfaces.requests.IReq_WebhookNotification>(
'webhookNotification',
conn,
);
req.fire({
connectionId,
provider,
event,
timestamp: Date.now(),
}).catch((err: any) => {
logger.warn(`Failed to notify client: ${err.message || err}`);
});
}
}
} catch (err: any) {
logger.warn(`Failed to broadcast webhook event: ${err.message || err}`);
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
});
logger.info('WebhookHandler routes registered');
}
}

View File

@@ -149,7 +149,7 @@ export class GiteaProvider extends BaseProvider {
private mapProject(r: plugins.giteaClient.IGiteaRepository): interfaces.data.IProject {
return {
id: String(r.id),
id: r.full_name || String(r.id),
name: r.name || '',
fullPath: r.full_name || '',
description: r.description || '',
@@ -164,7 +164,7 @@ export class GiteaProvider extends BaseProvider {
private mapGroup(o: plugins.giteaClient.IGiteaOrganization): interfaces.data.IGroup {
return {
id: String(o.id || o.name),
id: o.name || String(o.id),
name: o.name || '',
fullPath: o.name || '',
description: o.description || '',