diff --git a/backend/config/settings.py b/backend/config/settings.py index abcc4aa8..c8196868 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -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, + }, + }, +} diff --git a/backend/task/serializers.py b/backend/task/serializers.py index 26d98a6d..8ab19bd6 100644 --- a/backend/task/serializers.py +++ b/backend/task/serializers.py @@ -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 + ) diff --git a/backend/task/src/notify.py b/backend/task/src/notify.py index 3a9b1f23..7a493548 100644 --- a/backend/task/src/notify.py +++ b/backend/task/src/notify.py @@ -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]: diff --git a/backend/task/urls.py b/backend/task/urls.py index 66f985c6..d58e2aa7 100644 --- a/backend/task/urls.py +++ b/backend/task/urls.py @@ -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", + ), ] diff --git a/backend/task/views.py b/backend/task/views.py index ff2562e3..a0bdee77 100644 --- a/backend/task/views.py +++ b/backend/task/views.py @@ -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 + ) diff --git a/frontend/src/api/actions/testAppriseNotificationUrl.ts b/frontend/src/api/actions/testAppriseNotificationUrl.ts new file mode 100644 index 00000000..ff7f443e --- /dev/null +++ b/frontend/src/api/actions/testAppriseNotificationUrl.ts @@ -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('/api/task/notification/test/', { + method: 'POST', + body: { task_name: taskName, url }, + }); +}; + +export default testAppriseNotificationUrl; diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index 0712d07d..6b389e07 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -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} diff --git a/frontend/src/pages/SettingsScheduling.tsx b/frontend/src/pages/SettingsScheduling.tsx index 7ee95012..5d8084da 100644 --- a/frontend/src/pages/SettingsScheduling.tsx +++ b/frontend/src/pages/SettingsScheduling.tsx @@ -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(null); const [thumnailCheckError, setThumnailCheckError] = useState(null); const [zipBackupError, setZipBackupError] = useState(null); + const [testNotificationMessage, setTestNotificationMessage] = useState<{ + message: string; + success: boolean; + } | null>(null); const { data: appriseNotificationData } = appriseNotification ?? {}; @@ -539,49 +546,99 @@ const SettingsScheduling = () => {
-

- - Send notification on completed tasks with the help of the{' '} - - Apprise - {' '} - library. - -

+
+

+ + Send notification on completed tasks with the help of the{' '} + + Apprise + {' '} + library. + +

- + - { - setNotificationUrl(e.currentTarget.value); - }} - /> + { + setNotificationUrl(e.currentTarget.value); + }} + /> -
+ {testNotificationMessage && ( +
+

{testNotificationMessage.message}

+
+ )} +
diff --git a/frontend/src/style.css b/frontend/src/style.css index 135b6a6f..48037f6d 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -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;