Add test notification button (#996)
* Add api to test notifications before you save * Add button to UI * Inspect apprise logs to get errors * Better formatting around errors coming back from the test notification endpoint * Use apprise's built in log capture * Instruct the user to get error from container log instead of intercepting and parsing apprise logs * refac move to test method on notification class --------- Co-authored-by: Simon <simobilleter@gmail.com>
This commit is contained in:
parent
624a5f9bd4
commit
aefd678dca
|
|
@ -329,3 +329,21 @@ SPECTACULAR_SETTINGS = {
|
|||
"VERSION": TA_VERSION,
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
}
|
||||
|
||||
# Logging configuration
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"apprise": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,3 +91,12 @@ class TaskNotificationPostSerializer(serializers.Serializer):
|
|||
|
||||
task_name = serializers.ChoiceField(choices=list(TASK_CONFIG))
|
||||
url = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class TaskNotificationTestSerializer(serializers.Serializer):
|
||||
"""serialize task notification test POST"""
|
||||
|
||||
url = serializers.CharField()
|
||||
task_name = serializers.ChoiceField(
|
||||
choices=list(TASK_CONFIG), required=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,38 @@ class Notifications:
|
|||
|
||||
apobj.notify(body=body, title=title)
|
||||
|
||||
def test(self, url) -> tuple[bool, str]:
|
||||
"""send test notification"""
|
||||
try:
|
||||
apobj = apprise.Apprise()
|
||||
|
||||
if not apobj.add(url):
|
||||
success = False
|
||||
message = f"Invalid notification URL format: {url}"
|
||||
return success, message
|
||||
|
||||
title = f"[TA] {self.task_name} process ended with SUCCESS"
|
||||
body = "This is a test notification. Task completed successfully."
|
||||
|
||||
result = apobj.notify(body=body, title=title)
|
||||
|
||||
if result:
|
||||
success = True
|
||||
message = "Test notification sent successfully"
|
||||
return success, message
|
||||
|
||||
success = False
|
||||
message = (
|
||||
"Notification failed. "
|
||||
"Please check container logs for more information."
|
||||
)
|
||||
return success, message
|
||||
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
success = False
|
||||
message = f"Notification error: {str(err)}"
|
||||
return success, message
|
||||
|
||||
def _build_message(
|
||||
self, task_id: str, task_title: str
|
||||
) -> tuple[str, str | None]:
|
||||
|
|
|
|||
|
|
@ -34,4 +34,9 @@ urlpatterns = [
|
|||
views.ScheduleNotification.as_view(),
|
||||
name="api-schedule-notification",
|
||||
),
|
||||
path(
|
||||
"notification/test/",
|
||||
views.NotificationTestView.as_view(),
|
||||
name="api-schedule-notification-test",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from task.serializers import (
|
|||
TaskIDDataSerializer,
|
||||
TaskNotificationPostSerializer,
|
||||
TaskNotificationSerializer,
|
||||
TaskNotificationTestSerializer,
|
||||
TaskResultSerializer,
|
||||
)
|
||||
from task.src.config_schedule import CrontabValidator, ScheduleBuilder
|
||||
|
|
@ -343,3 +344,34 @@ class ScheduleNotification(ApiBaseView):
|
|||
Notifications(task_name).remove_task()
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class NotificationTestView(ApiBaseView):
|
||||
"""resolves to /api/task/notification/test/
|
||||
POST: test notification url
|
||||
"""
|
||||
|
||||
@extend_schema(
|
||||
request=TaskNotificationTestSerializer(),
|
||||
responses={
|
||||
200: OpenApiResponse(description="test notification sent"),
|
||||
400: OpenApiResponse(
|
||||
ErrorResponseSerializer(), description="bad request"
|
||||
),
|
||||
},
|
||||
)
|
||||
def post(self, request):
|
||||
"""test notification"""
|
||||
data_serializer = TaskNotificationTestSerializer(data=request.data)
|
||||
data_serializer.is_valid(raise_exception=True)
|
||||
validated_data = data_serializer.validated_data
|
||||
|
||||
url = validated_data["url"]
|
||||
task_name = validated_data.get("task_name", "manual_test")
|
||||
|
||||
success, message = Notifications(task_name).test(url)
|
||||
|
||||
status = 200 if success else 400
|
||||
return Response(
|
||||
{"success": success, "message": message}, status=status
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import APIClient from '../../functions/APIClient';
|
||||
import { AppriseTaskNameType } from './createAppriseNotificationUrl';
|
||||
|
||||
export type TestNotificationResponseType = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const testAppriseNotificationUrl = async (taskName: AppriseTaskNameType, url: string) => {
|
||||
return APIClient<TestNotificationResponseType>('/api/task/notification/test/', {
|
||||
method: 'POST',
|
||||
body: { task_name: taskName, url },
|
||||
});
|
||||
};
|
||||
|
||||
export default testAppriseNotificationUrl;
|
||||
|
|
@ -9,6 +9,7 @@ export interface ButtonProps {
|
|||
children?: string | ReactNode | ReactNode[];
|
||||
value?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ const Button = ({
|
|||
children,
|
||||
value,
|
||||
title,
|
||||
disabled,
|
||||
onClick,
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
|
|
@ -31,6 +33,7 @@ const Button = ({
|
|||
type={type}
|
||||
value={value}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import createTaskSchedule from '../api/actions/createTaskSchedule';
|
|||
import createAppriseNotificationUrl, {
|
||||
AppriseTaskNameType,
|
||||
} from '../api/actions/createAppriseNotificationUrl';
|
||||
import testAppriseNotificationUrl, {
|
||||
TestNotificationResponseType,
|
||||
} from '../api/actions/testAppriseNotificationUrl';
|
||||
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
||||
import { ApiError, ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
|
|
@ -62,6 +65,10 @@ const SettingsScheduling = () => {
|
|||
const [downloadPendingError, setDownloadPendingError] = useState<string | null>(null);
|
||||
const [thumnailCheckError, setThumnailCheckError] = useState<string | null>(null);
|
||||
const [zipBackupError, setZipBackupError] = useState<string | null>(null);
|
||||
const [testNotificationMessage, setTestNotificationMessage] = useState<{
|
||||
message: string;
|
||||
success: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const { data: appriseNotificationData } = appriseNotification ?? {};
|
||||
|
||||
|
|
@ -539,49 +546,99 @@ const SettingsScheduling = () => {
|
|||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<p>
|
||||
<i>
|
||||
Send notification on completed tasks with the help of the{' '}
|
||||
<a href="https://github.com/caronc/apprise" target="_blank">
|
||||
Apprise
|
||||
</a>{' '}
|
||||
library.
|
||||
</i>
|
||||
</p>
|
||||
<div>
|
||||
<p>
|
||||
<i>
|
||||
Send notification on completed tasks with the help of the{' '}
|
||||
<a href="https://github.com/caronc/apprise" target="_blank">
|
||||
Apprise
|
||||
</a>{' '}
|
||||
library.
|
||||
</i>
|
||||
</p>
|
||||
|
||||
<select
|
||||
value={notificationTask}
|
||||
onChange={e => {
|
||||
setNotificationTask(e.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
<option value="">-- select task --</option>
|
||||
<option value="update_subscribed">Rescan your Subscriptions</option>
|
||||
<option value="extract_download">Add to download queue</option>
|
||||
<option value="download_pending">Downloading</option>
|
||||
<option value="check_reindex">Reindex Documents</option>
|
||||
</select>
|
||||
<select
|
||||
value={notificationTask}
|
||||
onChange={e => {
|
||||
setNotificationTask(e.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
<option value="">-- select task --</option>
|
||||
<option value="update_subscribed">Rescan your Subscriptions</option>
|
||||
<option value="extract_download">Add to download queue</option>
|
||||
<option value="download_pending">Downloading</option>
|
||||
<option value="check_reindex">Reindex Documents</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Apprise notification URL"
|
||||
value={notificationUrl}
|
||||
onChange={e => {
|
||||
setNotificationUrl(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Apprise notification URL"
|
||||
value={notificationUrl}
|
||||
onChange={e => {
|
||||
setNotificationUrl(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Save"
|
||||
onClick={async () => {
|
||||
await createAppriseNotificationUrl(
|
||||
notificationTask as AppriseTaskNameType,
|
||||
notificationUrl || '',
|
||||
);
|
||||
<div className="button-box">
|
||||
<Button
|
||||
label="Save"
|
||||
disabled={!notificationUrl || !notificationTask}
|
||||
onClick={async () => {
|
||||
await createAppriseNotificationUrl(
|
||||
notificationTask as AppriseTaskNameType,
|
||||
notificationUrl || '',
|
||||
);
|
||||
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Test"
|
||||
disabled={!notificationUrl || !notificationTask}
|
||||
onClick={async () => {
|
||||
setTestNotificationMessage(null);
|
||||
try {
|
||||
const response: ApiResponseType<TestNotificationResponseType> =
|
||||
await testAppriseNotificationUrl(
|
||||
notificationTask as AppriseTaskNameType,
|
||||
notificationUrl || '',
|
||||
);
|
||||
if (response.data?.success) {
|
||||
setTestNotificationMessage({
|
||||
message: response.data.message,
|
||||
success: true,
|
||||
});
|
||||
} else {
|
||||
setTestNotificationMessage({
|
||||
message: response.data?.message || 'Test notification failed',
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const apiError = error as ApiError;
|
||||
if (apiError.status && apiError.message) {
|
||||
setTestNotificationMessage({ message: apiError.message, success: false });
|
||||
} else {
|
||||
setTestNotificationMessage({
|
||||
message: 'An unexpected error occurred while testing notification.',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
setRefresh(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{testNotificationMessage && (
|
||||
<div
|
||||
className={!testNotificationMessage.success ? 'danger-zone' : ''}
|
||||
style={{ margin: '10px 5px' }}
|
||||
>
|
||||
<p>{testNotificationMessage.message}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,17 @@ button:hover {
|
|||
color: var(--main-bg);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
button:disabled:hover {
|
||||
background-color: var(--accent-font-dark);
|
||||
transform: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.video-item.table {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
|
|
|||
Loading…
Reference in New Issue