feat(webhooks): add simple frontend webhook configuration UI (Phase 4)
All checks were successful
All checks were successful
This commit is contained in:
@@ -304,3 +304,158 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Webhooks Section Styles */
|
||||
.webhooks-section {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.webhooks-header {
|
||||
padding: 20px 30px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.webhooks-header:hover {
|
||||
background: #f0f1f2;
|
||||
}
|
||||
|
||||
.webhooks-header h2 {
|
||||
color: #333;
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.webhooks-toggle {
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.webhooks-toggle.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.webhooks-content {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.webhook-instance {
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.webhook-instance:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.webhook-instance h3 {
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.webhook-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-indicator.enabled {
|
||||
background: #e8f5e9;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.disabled {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.enable-webhook-btn {
|
||||
padding: 8px 16px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.enable-webhook-btn:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
.enable-webhook-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.test-webhook-btn {
|
||||
padding: 8px 16px;
|
||||
background: #f093fb;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.test-webhook-btn:hover {
|
||||
background: #d97ed8;
|
||||
}
|
||||
|
||||
.test-webhook-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.webhook-triggers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.trigger-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trigger-label {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.trigger-value {
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.trigger-value.active {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.trigger-value.inactive {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ function App() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [webhookSectionExpanded, setWebhookSectionExpanded] = useState(false);
|
||||
const [sonarrWebhook, setSonarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } });
|
||||
const [radarrWebhook, setRadarrWebhook] = useState({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } });
|
||||
const [webhookLoading, setWebhookLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
fetchWebhookStatus();
|
||||
}, []);
|
||||
|
||||
const fetchSessions = async () => {
|
||||
@@ -67,6 +72,112 @@ function App() {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const fetchWebhookStatus = async () => {
|
||||
try {
|
||||
// Fetch Sonarr notifications
|
||||
try {
|
||||
const sonarrResponse = await axios.get('/api/sonarr/notifications');
|
||||
const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
setSonarrWebhook({
|
||||
enabled: !!sonarrSofarr,
|
||||
triggers: sonarrSofarr ? {
|
||||
onGrab: sonarrSofarr.onGrab,
|
||||
onDownload: sonarrSofarr.onDownload,
|
||||
onImport: sonarrSofarr.onImport,
|
||||
onUpgrade: sonarrSofarr.onUpgrade
|
||||
} : { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }
|
||||
});
|
||||
} catch (err) {
|
||||
// Sonarr not configured or not accessible
|
||||
setSonarrWebhook({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } });
|
||||
}
|
||||
|
||||
// Fetch Radarr notifications
|
||||
try {
|
||||
const radarrResponse = await axios.get('/api/radarr/notifications');
|
||||
const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
setRadarrWebhook({
|
||||
enabled: !!radarrSofarr,
|
||||
triggers: radarrSofarr ? {
|
||||
onGrab: radarrSofarr.onGrab,
|
||||
onDownload: radarrSofarr.onDownload,
|
||||
onImport: radarrSofarr.onImport,
|
||||
onUpgrade: radarrSofarr.onUpgrade
|
||||
} : { onGrab: false, onDownload: false, onImport: false, onUpgrade: false }
|
||||
});
|
||||
} catch (err) {
|
||||
// Radarr not configured or not accessible
|
||||
setRadarrWebhook({ enabled: false, triggers: { onGrab: false, onDownload: false, onImport: false, onUpgrade: false } });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch webhook status:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const enableSonarrWebhook = async () => {
|
||||
setWebhookLoading(true);
|
||||
try {
|
||||
await axios.post('/api/sonarr/notifications/sofarr-webhook');
|
||||
await fetchWebhookStatus();
|
||||
} catch (err) {
|
||||
console.error('Failed to enable Sonarr webhook:', err);
|
||||
alert('Failed to enable Sonarr webhook. Check console for details.');
|
||||
} finally {
|
||||
setWebhookLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const enableRadarrWebhook = async () => {
|
||||
setWebhookLoading(true);
|
||||
try {
|
||||
await axios.post('/api/radarr/notifications/sofarr-webhook');
|
||||
await fetchWebhookStatus();
|
||||
} catch (err) {
|
||||
console.error('Failed to enable Radarr webhook:', err);
|
||||
alert('Failed to enable Radarr webhook. Check console for details.');
|
||||
} finally {
|
||||
setWebhookLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testSonarrWebhook = async () => {
|
||||
setWebhookLoading(true);
|
||||
try {
|
||||
const sonarrResponse = await axios.get('/api/sonarr/notifications');
|
||||
const sonarrSofarr = sonarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
if (sonarrSofarr) {
|
||||
await axios.post('/api/sonarr/notifications/test', { id: sonarrSofarr.id });
|
||||
alert('Sonarr webhook test sent successfully!');
|
||||
} else {
|
||||
alert('Sofarr webhook not configured for Sonarr.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to test Sonarr webhook:', err);
|
||||
alert('Failed to test Sonarr webhook. Check console for details.');
|
||||
} finally {
|
||||
setWebhookLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testRadarrWebhook = async () => {
|
||||
setWebhookLoading(true);
|
||||
try {
|
||||
const radarrResponse = await axios.get('/api/radarr/notifications');
|
||||
const radarrSofarr = radarrResponse.data.find(n => n.name === 'Sofarr');
|
||||
if (radarrSofarr) {
|
||||
await axios.post('/api/radarr/notifications/test', { id: radarrSofarr.id });
|
||||
alert('Radarr webhook test sent successfully!');
|
||||
} else {
|
||||
alert('Sofarr webhook not configured for Radarr.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to test Radarr webhook:', err);
|
||||
alert('Failed to test Radarr webhook. Check console for details.');
|
||||
} finally {
|
||||
setWebhookLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
@@ -178,6 +289,110 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="webhooks-section">
|
||||
<div className="webhooks-header" onClick={() => setWebhookSectionExpanded(!webhookSectionExpanded)}>
|
||||
<h2>⚡ Webhooks Configuration</h2>
|
||||
<span className={`webhooks-toggle ${webhookSectionExpanded ? 'expanded' : ''}`}>▼</span>
|
||||
</div>
|
||||
{webhookSectionExpanded && (
|
||||
<div className="webhooks-content">
|
||||
{webhookLoading && <div className="loading">Loading webhook status...</div>}
|
||||
<div className="webhook-instance">
|
||||
<h3>Sonarr</h3>
|
||||
<div className="webhook-status">
|
||||
<span className={`status-indicator ${sonarrWebhook.enabled ? 'enabled' : 'disabled'}`}>
|
||||
{sonarrWebhook.enabled ? '● Enabled' : '○ Disabled'}
|
||||
</span>
|
||||
{!sonarrWebhook.enabled && (
|
||||
<button onClick={enableSonarrWebhook} className="enable-webhook-btn" disabled={webhookLoading}>
|
||||
Enable Sofarr Webhooks
|
||||
</button>
|
||||
)}
|
||||
{sonarrWebhook.enabled && (
|
||||
<button onClick={testSonarrWebhook} className="test-webhook-btn" disabled={webhookLoading}>
|
||||
Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{sonarrWebhook.enabled && (
|
||||
<div className="webhook-triggers">
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Grab</span>
|
||||
<span className={`trigger-value ${sonarrWebhook.triggers.onGrab ? 'active' : 'inactive'}`}>
|
||||
{sonarrWebhook.triggers.onGrab ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Download</span>
|
||||
<span className={`trigger-value ${sonarrWebhook.triggers.onDownload ? 'active' : 'inactive'}`}>
|
||||
{sonarrWebhook.triggers.onDownload ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Import</span>
|
||||
<span className={`trigger-value ${sonarrWebhook.triggers.onImport ? 'active' : 'inactive'}`}>
|
||||
{sonarrWebhook.triggers.onImport ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Upgrade</span>
|
||||
<span className={`trigger-value ${sonarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'}`}>
|
||||
{sonarrWebhook.triggers.onUpgrade ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="webhook-instance">
|
||||
<h3>Radarr</h3>
|
||||
<div className="webhook-status">
|
||||
<span className={`status-indicator ${radarrWebhook.enabled ? 'enabled' : 'disabled'}`}>
|
||||
{radarrWebhook.enabled ? '● Enabled' : '○ Disabled'}
|
||||
</span>
|
||||
{!radarrWebhook.enabled && (
|
||||
<button onClick={enableRadarrWebhook} className="enable-webhook-btn" disabled={webhookLoading}>
|
||||
Enable Sofarr Webhooks
|
||||
</button>
|
||||
)}
|
||||
{radarrWebhook.enabled && (
|
||||
<button onClick={testRadarrWebhook} className="test-webhook-btn" disabled={webhookLoading}>
|
||||
Test
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{radarrWebhook.enabled && (
|
||||
<div className="webhook-triggers">
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Grab</span>
|
||||
<span className={`trigger-value ${radarrWebhook.triggers.onGrab ? 'active' : 'inactive'}`}>
|
||||
{radarrWebhook.triggers.onGrab ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Download</span>
|
||||
<span className={`trigger-value ${radarrWebhook.triggers.onDownload ? 'active' : 'inactive'}`}>
|
||||
{radarrWebhook.triggers.onDownload ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Import</span>
|
||||
<span className={`trigger-value ${radarrWebhook.triggers.onImport ? 'active' : 'inactive'}`}>
|
||||
{radarrWebhook.triggers.onImport ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trigger-item">
|
||||
<span className="trigger-label">On Upgrade</span>
|
||||
<span className={`trigger-value ${radarrWebhook.triggers.onUpgrade ? 'active' : 'inactive'}`}>
|
||||
{radarrWebhook.triggers.onUpgrade ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="app-footer">
|
||||
<p>Ensure your media is tagged with "user:username" in Sonarr/Radarr to match downloads to users.</p>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user