@@ -31,9 +31,18 @@ interface TelegramConfig {
3131 chatId : string | null ;
3232}
3333
34+ interface SlackConfig {
35+ configured : boolean ;
36+ enabled : boolean ;
37+ botToken : string | null ;
38+ appToken : string | null ;
39+ channelId : string | null ;
40+ }
41+
3442export default function IssuesConfigPage ( ) {
3543 const [ repos , setRepos ] = useState < Repository [ ] > ( [ ] ) ;
3644 const [ telegram , setTelegram ] = useState < TelegramConfig | null > ( null ) ;
45+ const [ slack , setSlack ] = useState < SlackConfig | null > ( null ) ;
3746 const [ loading , setLoading ] = useState ( true ) ;
3847 const [ error , setError ] = useState < string | null > ( null ) ;
3948 const [ success , setSuccess ] = useState < string | null > ( null ) ;
@@ -66,11 +75,17 @@ export default function IssuesConfigPage() {
6675 const [ tgChatTitle , setTgChatTitle ] = useState ( "" ) ;
6776 const [ tgManualChatId , setTgManualChatId ] = useState ( "" ) ;
6877
78+ const [ slackBotToken , setSlackBotToken ] = useState ( "" ) ;
79+ const [ slackAppToken , setSlackAppToken ] = useState ( "" ) ;
80+ const [ slackChannelId , setSlackChannelId ] = useState ( "" ) ;
81+ const [ savingSlack , setSavingSlack ] = useState ( false ) ;
82+
6983 async function fetchAll ( ) {
7084 try {
71- const [ repoRes , tgRes ] = await Promise . all ( [
85+ const [ repoRes , tgRes , slackRes ] = await Promise . all ( [
7286 fetch ( "/api/issues/projects" ) ,
7387 fetch ( "/api/issues/telegram" ) ,
88+ fetch ( "/api/issues/slack" ) ,
7489 ] ) ;
7590 if ( repoRes . ok ) {
7691 const data = await repoRes . json ( ) ;
@@ -80,6 +95,10 @@ export default function IssuesConfigPage() {
8095 const data = await tgRes . json ( ) ;
8196 setTelegram ( data ) ;
8297 }
98+ if ( slackRes . ok ) {
99+ const data = await slackRes . json ( ) ;
100+ setSlack ( data ) ;
101+ }
83102 } catch {
84103 setError ( "Could not connect to server" ) ;
85104 } finally {
@@ -285,6 +304,52 @@ export default function IssuesConfigPage() {
285304 }
286305 }
287306
307+ async function handleSaveSlack ( ) {
308+ setSavingSlack ( true ) ;
309+ setError ( null ) ;
310+ try {
311+ const res = await fetch ( "/api/issues/slack" , {
312+ method : "POST" ,
313+ headers : { "Content-Type" : "application/json" } ,
314+ body : JSON . stringify ( {
315+ botToken : slackBotToken . trim ( ) ,
316+ appToken : slackAppToken . trim ( ) ,
317+ channelId : slackChannelId . trim ( ) || undefined ,
318+ test : true ,
319+ } ) ,
320+ } ) ;
321+ if ( ! res . ok ) {
322+ const data = await res . json ( ) ;
323+ showError ( data . error || "Failed to save Slack config" ) ;
324+ return ;
325+ }
326+
327+ setSlackBotToken ( "" ) ;
328+ setSlackAppToken ( "" ) ;
329+ setSlackChannelId ( "" ) ;
330+ showSuccess ( "Slack app configured and tested" ) ;
331+ await fetchAll ( ) ;
332+ } catch {
333+ showError ( "Failed to save Slack config" ) ;
334+ } finally {
335+ setSavingSlack ( false ) ;
336+ }
337+ }
338+
339+ async function handleDeleteSlack ( ) {
340+ try {
341+ const res = await fetch ( "/api/issues/slack" , { method : "DELETE" } ) ;
342+ if ( ! res . ok ) {
343+ showError ( "Failed to remove Slack config" ) ;
344+ return ;
345+ }
346+ showSuccess ( "Slack config removed" ) ;
347+ await fetchAll ( ) ;
348+ } catch {
349+ showError ( "Failed to remove Slack config" ) ;
350+ }
351+ }
352+
288353 if ( loading ) {
289354 return (
290355 < div className = "flex items-center justify-center h-full" >
@@ -332,6 +397,100 @@ export default function IssuesConfigPage() {
332397 </ div >
333398 ) }
334399
400+ < section className = "space-y-3" >
401+ < h2 className = "text-[14px] font-mono font-bold text-accent uppercase tracking-widest" >
402+ > Slack Issue App
403+ </ h2 >
404+ < p className = "text-[13px] font-mono text-muted-foreground ml-4" >
405+ Uses Slack Socket Mode so issue creation and all follow-up replies stay inside the Slack thread.
406+ </ p >
407+
408+ { slack ?. configured ? (
409+ < div className = "term-card p-4 space-y-2" >
410+ < div className = "grid grid-cols-3 gap-4" >
411+ < div className = "space-y-1" >
412+ < div className = "text-[12px] font-mono text-muted uppercase" > bot token</ div >
413+ < div className = "text-[14px] font-mono text-foreground" > { slack . botToken } </ div >
414+ </ div >
415+ < div className = "space-y-1" >
416+ < div className = "text-[12px] font-mono text-muted uppercase" > app token</ div >
417+ < div className = "text-[14px] font-mono text-foreground" > { slack . appToken } </ div >
418+ </ div >
419+ < div className = "space-y-1 text-right" >
420+ < div className = "text-[12px] font-mono text-muted uppercase" > channel id</ div >
421+ < div className = "text-[14px] font-mono text-foreground" > { slack . channelId || "any joined channel" } </ div >
422+ </ div >
423+ </ div >
424+ < div className = "flex items-center justify-between pt-2 border-t border-border/40" >
425+ < span className = "flex items-center gap-1.5 text-[12px] font-mono text-green" >
426+ < span className = "h-1.5 w-1.5 rounded-full bg-green" />
427+ configured
428+ </ span >
429+ < button
430+ onClick = { handleDeleteSlack }
431+ className = "text-[12px] font-mono text-muted-foreground hover:text-red transition-colors"
432+ >
433+ remove
434+ </ button >
435+ </ div >
436+ </ div >
437+ ) : (
438+ < div className = "term-card p-4 space-y-3" >
439+ < div className = "space-y-1" >
440+ < label className = "text-[12px] font-mono text-muted uppercase tracking-wider" > bot token</ label >
441+ < input
442+ type = "password"
443+ value = { slackBotToken }
444+ onChange = { ( e ) => setSlackBotToken ( e . target . value ) }
445+ placeholder = "xoxb-..."
446+ className = "w-full border border-border bg-background px-3 py-2 text-[14px] font-mono text-foreground placeholder:text-muted/50 outline-none focus:border-accent"
447+ />
448+ </ div >
449+ < div className = "space-y-1" >
450+ < label className = "text-[12px] font-mono text-muted uppercase tracking-wider" > app token</ label >
451+ < input
452+ type = "password"
453+ value = { slackAppToken }
454+ onChange = { ( e ) => setSlackAppToken ( e . target . value ) }
455+ placeholder = "xapp-..."
456+ className = "w-full border border-border bg-background px-3 py-2 text-[14px] font-mono text-foreground placeholder:text-muted/50 outline-none focus:border-accent"
457+ />
458+ < p className = "text-[12px] font-mono text-muted" >
459+ Enable Socket Mode in your Slack app and use an app-level token with the < span className = "text-foreground" > connections:write</ span > scope.
460+ </ p >
461+ </ div >
462+ < div className = "space-y-1" >
463+ < label className = "text-[12px] font-mono text-muted uppercase tracking-wider" > channel id (optional)</ label >
464+ < input
465+ type = "text"
466+ value = { slackChannelId }
467+ onChange = { ( e ) => setSlackChannelId ( e . target . value ) }
468+ placeholder = "C0123456789"
469+ className = "w-full border border-border bg-background px-3 py-2 text-[14px] font-mono text-foreground placeholder:text-muted/50 outline-none focus:border-accent"
470+ />
471+ < p className = "text-[12px] font-mono text-muted" >
472+ Restrict issue creation to one Slack channel, or leave blank to accept mentions in any channel the bot has joined.
473+ </ p >
474+ </ div >
475+ < div className = "border border-border/50 bg-background/40 px-3 py-3 text-[12px] font-mono text-muted space-y-1" >
476+ < div > Required bot scopes: < span className = "text-foreground" > app_mentions:read</ span > , < span className = "text-foreground" > channels:history</ span > , < span className = "text-foreground" > groups:history</ span > , < span className = "text-foreground" > chat:write</ span > </ div >
477+ < div > Usage: mention the bot with < span className = "text-foreground" > @bot repo-name: description</ span > . All replies should stay in that Slack thread.</ div >
478+ </ div >
479+ < button
480+ onClick = { handleSaveSlack }
481+ disabled = { ! slackBotToken . trim ( ) || ! slackAppToken . trim ( ) || savingSlack }
482+ className = "flex items-center gap-1.5 border border-accent bg-accent/10 px-4 py-1.5 text-[13px] font-mono font-bold text-accent uppercase tracking-wider transition-all hover:bg-accent/20 disabled:opacity-40"
483+ >
484+ { savingSlack ? (
485+ < > < Loader2 className = "h-3 w-3 animate-spin" /> saving...</ >
486+ ) : (
487+ < > < Send className = "h-3 w-3" /> save & test</ >
488+ ) }
489+ </ button >
490+ </ div >
491+ ) }
492+ </ section >
493+
335494 { /* Telegram Bot Config */ }
336495 < section className = "space-y-3" >
337496 < h2 className = "text-[14px] font-mono font-bold text-accent uppercase tracking-widest" >
0 commit comments