Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
afa016c
feat: add incident.io connector backend
beng360 Apr 20, 2026
ea966b1
feat: add incident.io connector frontend
beng360 Apr 20, 2026
ab5f81d
feat: add incident.io skill definition for RCA agent
beng360 Apr 20, 2026
aab5c6e
fix: harden incident.io connector end-to-end
beng360 Apr 20, 2026
6776ec9
fix: resolve CodeQL and code quality issues on incident.io connector
beng360 Apr 20, 2026
1dbaa28
fix: remaining CodeRabbit issues — fuzzy match and API filter params
beng360 Apr 20, 2026
d0d501c
remove unsued logos
OlivierTrudeau Apr 21, 2026
ac5c660
Fix damian wrong ordering of creation of tables
OlivierTrudeau Apr 21, 2026
1f81ad4
fix: resolve remaining CodeRabbit review comments on incident.io conn…
beng360 Apr 21, 2026
7d3161c
fix: address Olivier's review — remove Next.js webhook proxy, add mis…
beng360 Apr 21, 2026
afc897d
refactor: use ConnectorRegistry for source display names instead of h…
beng360 Apr 21, 2026
8a419bc
Merge remote-tracking branch 'origin/main' into feat/incidentio-conne…
beng360 Apr 21, 2026
dc7b43e
fix logo
OlivierTrudeau Apr 21, 2026
a96cc94
Remove prefix matching from load_skill fuzzy match
beng360 Apr 21, 2026
751297c
Address PR #287 review: webhook signing, pagination, UX fixes
beng360 Apr 21, 2026
9dd0e5a
Fix CodeQL and SonarCloud security findings
beng360 Apr 21, 2026
7405379
Harden incidentio routes against log injection and info exposure
beng360 Apr 21, 2026
51cb51c
Fix SonarCloud CSRF hotspots and reduce code duplication
beng360 Apr 21, 2026
ffa7e3b
Extract useConnectorAuth hook to reduce auth page duplication
beng360 Apr 21, 2026
7c59994
Fix infinite re-render loop in useConnectorAuth
beng360 Apr 21, 2026
0f2dd90
Fix incident.io field extraction and org-scoped preference storage
beng360 Apr 21, 2026
e116c5d
Remove user-controlled data from log statements in store_user_preference
beng360 Apr 22, 2026
e786673
Merge remote-tracking branch 'origin/main' into feat/incidentio-conne…
beng360 Apr 22, 2026
242a086
Fix incident.io webhook: add to open prefixes, match v2 event types, …
beng360 Apr 22, 2026
4f9b1d1
Clarify webhook signing secret description in incident.io setup
beng360 Apr 22, 2026
9a11b7f
Show webhook signing secret saved state in incident.io setup UI
beng360 Apr 22, 2026
3bbbadf
Fix all 23 SonarCloud quality gate issues on PR #287
beng360 Apr 23, 2026
779d74e
Fix 2 SonarCloud reliability bugs (S7727, S7502) to pass quality gate
beng360 Apr 23, 2026
b16a15a
Merge remote-tracking branch 'origin/main' into feat/incidentio-conne…
beng360 Apr 23, 2026
f803698
Merge commit '0bdf89b09245ba2ead91b8c69616a85d3226105c' into feat/inc…
OlivierTrudeau Apr 23, 2026
909f7ac
Fix stale cache showing false "connected" state and use set_rls_conte…
beng360 Apr 23, 2026
ff7d167
Merge remote-tracking branch 'origin/main' into feat/incidentio-conne…
beng360 Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/public/incidentio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 21 additions & 1 deletion client/src/app/api/connected-accounts/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function DELETE(
const { provider } = await context.params

// Validate provider
if (!['gcp', 'azure', 'aws', 'github', 'grafana', 'datadog', 'netdata', 'ovh', 'scaleway', 'tailscale', 'slack', 'google_chat', 'splunk', 'dynatrace', 'confluence', 'jira', 'sharepoint', 'coroot', 'thousandeyes', 'jenkins', 'cloudbees', 'bigpanda', 'spinnaker', 'newrelic', 'opsgenie'].includes(provider)) {
if (!['gcp', 'azure', 'aws', 'github', 'grafana', 'datadog', 'netdata', 'ovh', 'scaleway', 'tailscale', 'slack', 'google_chat', 'splunk', 'dynatrace', 'confluence', 'jira', 'sharepoint', 'coroot', 'thousandeyes', 'jenkins', 'cloudbees', 'bigpanda', 'spinnaker', 'newrelic', 'opsgenie', 'incidentio'].includes(provider)) {
return NextResponse.json(
{ error: 'Invalid provider' },
{ status: 400 }
Expand Down Expand Up @@ -442,6 +442,26 @@ export async function DELETE(
return NextResponse.json({ success: true })
}

// Special handling for incident.io
if (provider === 'incidentio') {
const response = await fetch(`${API_BASE_URL}/incidentio/disconnect`, {
method: 'DELETE',
headers: authHeaders,
})

if (!response.ok) {
const errorText = await response.text()
console.error('Backend error disconnecting incident.io:', errorText)
return NextResponse.json(
{ error: 'Failed to disconnect incident.io' },
{ status: response.status }
)
}

const data = await response.json()
return NextResponse.json(data)
}

// Special handling for BigPanda
if (provider === 'bigpanda') {
const response = await fetch(`${API_BASE_URL}/bigpanda/disconnect`, {
Expand Down
54 changes: 54 additions & 0 deletions client/src/app/api/incident-io/alerts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthenticatedUser } from '@/lib/auth-helper';

const API_BASE_URL = process.env.BACKEND_URL;
const FETCH_TIMEOUT_MS = 15000;

export async function GET(request: NextRequest) {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;
const { searchParams } = new URL(request.url);
const limit = searchParams.get('limit') || '50';
const offset = searchParams.get('offset') || '0';
const severity = searchParams.get('severity');

const params = new URLSearchParams({ limit, offset });
if (severity) params.append('severity', severity);

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/alerts?${params}`, {
method: 'GET',
headers: authHeaders,
credentials: 'include',
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/alerts] Backend error:', text);
return NextResponse.json({ error: 'Failed to get incidents' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Request timeout' }, { status: 504 });
}
console.error('[api/incident-io/alerts] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to get incidents' }, { status: 500 });
}
}
47 changes: 47 additions & 0 deletions client/src/app/api/incident-io/alerts/webhook-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import { getAuthenticatedUser } from '@/lib/auth-helper';

const API_BASE_URL = process.env.BACKEND_URL;
const FETCH_TIMEOUT_MS = 15000;

Comment thread
OlivierTrudeau marked this conversation as resolved.
export async function GET() {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/alerts/webhook-url`, {
method: 'GET',
headers: authHeaders,
credentials: 'include',
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/alerts/webhook-url] Backend error:', text);
return NextResponse.json({ error: 'Failed to get webhook URL' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Request timeout' }, { status: 504 });
}
console.error('[api/incident-io/alerts/webhook-url] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to get webhook URL' }, { status: 500 });
}
}
52 changes: 52 additions & 0 deletions client/src/app/api/incident-io/connect/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthenticatedUser } from '@/lib/auth-helper';

const API_BASE_URL = process.env.BACKEND_URL;
const FETCH_TIMEOUT_MS = 15000;

export async function POST(request: NextRequest) {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;
const payload = await request.json();

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/connect`, {
method: 'POST',
headers: {
...authHeaders,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
credentials: 'include',
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/connect] Backend error:', text);
return NextResponse.json({ error: 'Failed to connect to incident.io' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Connection timeout' }, { status: 504 });
}
console.error('[api/incident-io/connect] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to connect to incident.io' }, { status: 500 });
}
}
94 changes: 94 additions & 0 deletions client/src/app/api/incident-io/rca-settings/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { NextResponse } from 'next/server';
import { getAuthenticatedUser } from '@/lib/auth-helper';

const API_BASE_URL = process.env.BACKEND_URL;
const FETCH_TIMEOUT_MS = 15000;

export async function GET() {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/rca-settings`, {
method: 'GET',
headers: authHeaders,
credentials: 'include',
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/rca-settings] Backend error:', text);
return NextResponse.json({ error: 'Failed to get RCA settings' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Request timeout' }, { status: 504 });
}
console.error('[api/incident-io/rca-settings] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to get RCA settings' }, { status: 500 });
}
}

export async function PUT(request: Request) {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;
const body = await request.json();

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/rca-settings`, {
method: 'PUT',
headers: {
...authHeaders,
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(body),
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/rca-settings] Backend error:', text);
return NextResponse.json({ error: 'Failed to update RCA settings' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Request timeout' }, { status: 504 });
}
console.error('[api/incident-io/rca-settings] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to update RCA settings' }, { status: 500 });
}
}
47 changes: 47 additions & 0 deletions client/src/app/api/incident-io/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from 'next/server';
import { getAuthenticatedUser } from '@/lib/auth-helper';

const API_BASE_URL = process.env.BACKEND_URL;
const FETCH_TIMEOUT_MS = 15000;

export async function GET() {
try {
const authResult = await getAuthenticatedUser();

if (authResult instanceof NextResponse) {
return authResult;
}

const { headers: authHeaders } = authResult;

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(`${API_BASE_URL}/incidentio/status`, {
method: 'GET',
headers: authHeaders,
credentials: 'include',
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}

if (!response.ok) {
const text = await response.text();
console.error('[api/incident-io/status] Backend error:', text);
return NextResponse.json({ error: 'Failed to get incident.io status' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return NextResponse.json({ error: 'Request timeout' }, { status: 504 });
}
console.error('[api/incident-io/status] Error:', error instanceof Error ? error.message : 'Unknown error');
return NextResponse.json({ error: 'Failed to get incident.io status' }, { status: 500 });
}
}
Loading
Loading