feat(webhooks): add simple frontend webhook configuration UI (Phase 4)
This commit is contained in:
@@ -304,3 +304,158 @@ body {
|
|||||||
grid-template-columns: 1fr;
|
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 [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [sessions, setSessions] = useState([]);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchSessions();
|
fetchSessions();
|
||||||
|
fetchWebhookStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchSessions = async () => {
|
const fetchSessions = async () => {
|
||||||
@@ -67,6 +72,112 @@ function App() {
|
|||||||
return new Date(dateString).toLocaleString();
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@@ -178,6 +289,110 @@ function App() {
|
|||||||
</div>
|
</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">
|
<footer className="app-footer">
|
||||||
<p>Ensure your media is tagged with "user:username" in Sonarr/Radarr to match downloads to users.</p>
|
<p>Ensure your media is tagged with "user:username" in Sonarr/Radarr to match downloads to users.</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
Reference in New Issue
Block a user