Feat: Add new PATCH api for updating the task and improved the no timer messaging
This commit is contained in:
parent
b33cb0a845
commit
ddc71cc1bd
5 changed files with 396 additions and 43 deletions
73
API.md
73
API.md
|
|
@ -37,7 +37,7 @@ This document describes the available endpoints and their usage. All endpoints r
|
|||
- [POST /api/tasks/{id}/mark](#post-apitasksidmark)
|
||||
- [DELETE /api/tasks/{id}/mark](#delete-apitasksidmark)
|
||||
- [POST /api/tasks/{id}/metadata](#post-apitasksidmetadata)
|
||||
- [POST /api/tasks/{id}/toggle](#post-apitasksidtoggle)
|
||||
- [PATCH /api/tasks/{id}](#patch-apitasksid)
|
||||
- [GET /api/task\_definitions/](#get-apitask_definitions)
|
||||
- [GET /api/task\_definitions/{identifier}](#get-apitask_definitionsidentifier)
|
||||
- [POST /api/task\_definitions/](#post-apitask_definitions)
|
||||
|
|
@ -783,11 +783,29 @@ or on error
|
|||
{
|
||||
"url": "https://example.com/feed", // required, validated URL
|
||||
"preset": "news-preset", // optional preset override - In real scenarios, the preset come from the task.
|
||||
"handler": "GenericTaskHandler" // optional explicit handler class name, in real scenarios, the handler is resolved automatically.
|
||||
"handler": "GenericTaskHandler", // optional explicit handler class name, in real scenarios, the handler is resolved automatically.
|
||||
"static_only": false // optional, if true performs only can_handle check without extracting items (faster, lightweight)
|
||||
}
|
||||
```
|
||||
|
||||
**Response (success)**:
|
||||
**Notes**:
|
||||
- When `static_only` is `true`, the endpoint only checks if a handler can process the URL using the `can_handle` method
|
||||
- This is much faster and lighter than the full inspection which calls the handler's `extract` method
|
||||
- Use `static_only: true` for quick validation checks (e.g., UI indicators)
|
||||
- Use `static_only: false` (default) for full inspection with item extraction
|
||||
|
||||
**Response (success with static_only: true)**:
|
||||
```json
|
||||
{
|
||||
"items": [],
|
||||
"metadata": {
|
||||
"matched": true,
|
||||
"handler": "GenericTaskHandler"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (success with static_only: false)**:
|
||||
```json
|
||||
{
|
||||
"matched": true,
|
||||
|
|
@ -924,42 +942,63 @@ or
|
|||
|
||||
---
|
||||
|
||||
### POST /api/tasks/{id}/toggle
|
||||
**Purpose**: Toggle the enabled/disabled status of a scheduled task.
|
||||
### PATCH /api/tasks/{id}
|
||||
**Purpose**: Update specific fields of a scheduled task.
|
||||
|
||||
**Path Parameter**:
|
||||
- `id`: Task ID.
|
||||
|
||||
**Request Body**:
|
||||
JSON object with fields to update:
|
||||
```json
|
||||
{
|
||||
"enabled": false,
|
||||
"handler_enabled": true,
|
||||
"timer": "0 */6 * * *",
|
||||
"preset": "audio",
|
||||
"folder": "downloads/music",
|
||||
"template": "%(title)s.%(ext)s",
|
||||
"cli": "--extract-audio",
|
||||
"auto_start": true
|
||||
}
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- Only include fields you want to update
|
||||
- All fields are optional
|
||||
|
||||
**Response**:
|
||||
Returns the updated task object:
|
||||
Returns the updated task
|
||||
```json
|
||||
{
|
||||
"id": "task_id",
|
||||
"name": "Task Name",
|
||||
"url": "https://example.com/playlist",
|
||||
"preset": "default",
|
||||
"folder": "",
|
||||
"template": "",
|
||||
"cli": "",
|
||||
"preset": "audio",
|
||||
"folder": "downloads/music",
|
||||
"template": "%(title)s.%(ext)s",
|
||||
"cli": "--extract-audio",
|
||||
"timer": "0 */6 * * *",
|
||||
"auto_start": true,
|
||||
"handler_enabled": true,
|
||||
"enabled": false
|
||||
}
|
||||
```
|
||||
or
|
||||
|
||||
**Error Responses**:
|
||||
```json
|
||||
{ "error": "Task 'task_id' does not exist." }
|
||||
{ "error": "Invalid JSON in request body.", "message": "..." }
|
||||
{ "error": "Request body must be a JSON object." }
|
||||
{ "error": "No valid fields to update." }
|
||||
{ "error": "Validation failed: Invalid timer format." }
|
||||
```
|
||||
|
||||
**Notes**:
|
||||
- When a task is disabled (`enabled: false`), it will not be executed by the scheduler
|
||||
|
||||
**Status Codes**:
|
||||
- `200 OK` - Task status toggled successfully, returns updated task object
|
||||
- `400 Bad Request` - Missing task ID
|
||||
- `200 OK` - Task updated successfully
|
||||
- `400 Bad Request` - Missing task ID, invalid JSON, no valid fields, or validation error
|
||||
- `404 Not Found` - Task does not exist
|
||||
- `500 Internal Server Error` - Failed to update task status
|
||||
- `500 Internal Server Error` - Failed to update task
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -884,6 +884,7 @@ class HandleTask:
|
|||
url: str,
|
||||
preset: str | None = None,
|
||||
handler_name: str | None = None,
|
||||
static_only: bool = False,
|
||||
) -> TaskResult | TaskFailure:
|
||||
if not self._handlers:
|
||||
self._handlers = self._discover()
|
||||
|
|
@ -937,6 +938,9 @@ class HandleTask:
|
|||
|
||||
base_metadata: dict[str, Any] = {"matched": True, "handler": handler_cls.__name__}
|
||||
|
||||
if static_only:
|
||||
return TaskResult(items=[], metadata=base_metadata)
|
||||
|
||||
try:
|
||||
extraction: TaskResult | TaskFailure = await services.handle_async(
|
||||
handler=handler_cls.extract, task=task, config=self._config
|
||||
|
|
|
|||
|
|
@ -20,22 +20,38 @@ LOG: logging.Logger = logging.getLogger(__name__)
|
|||
|
||||
@route("POST", "api/tasks/inspect", "task_handler_inspect")
|
||||
async def task_handler_inspect(request: Request, tasks: Tasks, encoder: Encoder, config: Config) -> Response:
|
||||
"""
|
||||
Check if handler can process the given URL.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
tasks (Tasks): The tasks instance.
|
||||
encoder (Encoder): The encoder instance.
|
||||
config (Config): The config instance.
|
||||
|
||||
Returns:
|
||||
Response: The response object.
|
||||
|
||||
"""
|
||||
data = await request.json()
|
||||
|
||||
url: str | None = data.get("url") if isinstance(data, dict) else None
|
||||
if not url:
|
||||
return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code)
|
||||
try:
|
||||
validate_url(url, allow_internal=config.allow_internal_urls)
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
static_only: bool = data.get("static_only", False) if isinstance(data, dict) else False
|
||||
if not static_only:
|
||||
try:
|
||||
validate_url(url, allow_internal=config.allow_internal_urls)
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
preset: str = data.get("preset", "") if isinstance(data, dict) else ""
|
||||
handler_name: str | None = data.get("handler") if isinstance(data, dict) else None
|
||||
|
||||
try:
|
||||
result: TaskResult | TaskFailure = await tasks.get_handler().inspect(
|
||||
url=url, preset=preset, handler_name=handler_name
|
||||
url=url, preset=preset, handler_name=handler_name, static_only=static_only
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
|
|
@ -345,10 +361,10 @@ async def task_metadata(request: Request, config: Config, encoder: Encoder) -> R
|
|||
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
|
||||
@route("POST", "api/tasks/{id}/toggle", "tasks_toggle_enabled")
|
||||
async def task_toggle_enabled(request: Request, encoder: Encoder) -> Response:
|
||||
@route("PATCH", "api/tasks/{id}", "tasks_update")
|
||||
async def task_update(request: Request, encoder: Encoder) -> Response:
|
||||
"""
|
||||
Toggle the enabled status of a task.
|
||||
Update specific fields of a task.
|
||||
|
||||
Args:
|
||||
request (Request): The request object.
|
||||
|
|
@ -358,11 +374,23 @@ async def task_toggle_enabled(request: Request, encoder: Encoder) -> Response:
|
|||
Response: The response object
|
||||
|
||||
"""
|
||||
task_id: str = request.match_info.get("id", None)
|
||||
|
||||
if not task_id:
|
||||
if not (task_id := request.match_info.get("id", None)):
|
||||
return web.json_response(data={"error": "No task id."}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception as e:
|
||||
return web.json_response(
|
||||
data={"error": "Invalid JSON in request body.", "message": str(e)},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return web.json_response(
|
||||
{"error": "Request body must be a JSON object."},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
tasks: Tasks = Tasks.get_instance()
|
||||
try:
|
||||
task: Task | None = tasks.get(task_id)
|
||||
|
|
@ -371,13 +399,34 @@ async def task_toggle_enabled(request: Request, encoder: Encoder) -> Response:
|
|||
data={"error": f"Task '{task_id}' does not exist."}, status=web.HTTPNotFound.status_code
|
||||
)
|
||||
|
||||
task.enabled = not task.enabled
|
||||
task_dict: dict = task.serialize()
|
||||
protected_fields: set[str] = {"id"}
|
||||
updated: bool = False
|
||||
|
||||
for key, value in data.items():
|
||||
if key in protected_fields or key not in task_dict:
|
||||
continue
|
||||
|
||||
setattr(task, key, value)
|
||||
updated = True
|
||||
|
||||
if not updated:
|
||||
return web.json_response(
|
||||
data={"error": "No valid fields to update."},
|
||||
status=web.HTTPBadRequest.status_code,
|
||||
)
|
||||
|
||||
try:
|
||||
Tasks.validate(task)
|
||||
except ValueError as e:
|
||||
return web.json_response({"error": f"Validation failed: {e!s}"}, status=web.HTTPBadRequest.status_code)
|
||||
|
||||
tasks.save(tasks=tasks.get_all()).load()
|
||||
|
||||
return web.json_response(data=tasks.get(task_id), status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
return web.json_response(data=task, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
return web.json_response(
|
||||
data={"error": "Failed to toggle task status.", "message": str(e)},
|
||||
data={"error": "Failed to update task.", "message": str(e)},
|
||||
status=web.HTTPInternalServerError.status_code,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -921,3 +921,156 @@ class TestTasks:
|
|||
|
||||
# Verify download queue was NOT called (task was skipped)
|
||||
mock_download_queue_instance.add.assert_not_called()
|
||||
|
||||
|
||||
class TestHandleTaskInspect:
|
||||
"""Test the HandleTask inspect method with static_only parameter."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.library.Tasks.Config")
|
||||
@patch("app.library.Tasks.EventBus")
|
||||
@patch("app.library.Tasks.Scheduler")
|
||||
@patch("app.library.Tasks.DownloadQueue")
|
||||
@patch("app.library.Tasks.Services")
|
||||
async def test_inspect_with_static_only_true(
|
||||
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
||||
):
|
||||
"""Test inspect with static_only=True returns immediately after can_handle check."""
|
||||
mock_config_instance = Mock(
|
||||
debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *"
|
||||
)
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_scheduler.get_instance.return_value = Mock()
|
||||
mock_download_queue.get_instance.return_value = Mock()
|
||||
|
||||
# Mock Services to simulate successful can_handle
|
||||
mock_services_instance = Mock()
|
||||
mock_services_instance.handle_sync = Mock(return_value=True)
|
||||
mock_services_instance.handle_async = AsyncMock() # Should NOT be called with static_only=True
|
||||
mock_services.get_instance.return_value = mock_services_instance
|
||||
|
||||
# Create a mock handler
|
||||
mock_handler = Mock()
|
||||
mock_handler.__name__ = "TestHandler"
|
||||
mock_handler.can_handle = Mock(return_value=True)
|
||||
mock_handler.extract = AsyncMock() # Should NOT be called with static_only=True
|
||||
|
||||
tasks = Tasks(file=None, config=mock_config_instance)
|
||||
handler = tasks.get_handler()
|
||||
handler._handlers = [mock_handler]
|
||||
|
||||
# Call inspect with static_only=True
|
||||
result = await handler.inspect(
|
||||
url="https://example.com/feed", preset="default", handler_name=None, static_only=True
|
||||
)
|
||||
|
||||
# Verify result structure
|
||||
assert hasattr(result, "items")
|
||||
assert hasattr(result, "metadata")
|
||||
assert result.items == []
|
||||
assert result.metadata["matched"] is True
|
||||
assert result.metadata["handler"] == "TestHandler"
|
||||
|
||||
# Verify handle_async (extract) was NOT called
|
||||
mock_services_instance.handle_async.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.library.Tasks.Config")
|
||||
@patch("app.library.Tasks.EventBus")
|
||||
@patch("app.library.Tasks.Scheduler")
|
||||
@patch("app.library.Tasks.DownloadQueue")
|
||||
@patch("app.library.Tasks.Services")
|
||||
async def test_inspect_with_static_only_false(
|
||||
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
||||
):
|
||||
"""Test inspect with static_only=False performs full extraction."""
|
||||
from app.library.Tasks import TaskResult
|
||||
|
||||
mock_config_instance = Mock(
|
||||
debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *"
|
||||
)
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_scheduler.get_instance.return_value = Mock()
|
||||
mock_download_queue.get_instance.return_value = Mock()
|
||||
|
||||
# Mock Services
|
||||
mock_services_instance = Mock()
|
||||
mock_services_instance.handle_sync = Mock(return_value=True)
|
||||
|
||||
# Mock successful extract result
|
||||
mock_extract_result = TaskResult(
|
||||
items=[{"url": "https://example.com/video1", "title": "Video 1"}],
|
||||
metadata={"raw_count": 1},
|
||||
)
|
||||
mock_services_instance.handle_async = AsyncMock(return_value=mock_extract_result)
|
||||
mock_services.get_instance.return_value = mock_services_instance
|
||||
|
||||
# Create a mock handler
|
||||
mock_handler = Mock()
|
||||
mock_handler.__name__ = "TestHandler"
|
||||
mock_handler.can_handle = Mock(return_value=True)
|
||||
mock_handler.extract = AsyncMock(return_value=mock_extract_result)
|
||||
|
||||
tasks = Tasks(file=None, config=mock_config_instance)
|
||||
handler = tasks.get_handler()
|
||||
handler._handlers = [mock_handler]
|
||||
|
||||
# Call inspect with static_only=False (default)
|
||||
result = await handler.inspect(url="https://example.com/feed", preset="default", handler_name=None)
|
||||
|
||||
# Verify result structure includes extracted items
|
||||
assert hasattr(result, "items")
|
||||
assert hasattr(result, "metadata")
|
||||
assert len(result.items) > 0
|
||||
assert result.metadata["matched"] is True
|
||||
assert result.metadata["handler"] == "TestHandler"
|
||||
assert result.metadata["supported"] is True
|
||||
|
||||
# Verify handle_async (extract) WAS called
|
||||
mock_services_instance.handle_async.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("app.library.Tasks.Config")
|
||||
@patch("app.library.Tasks.EventBus")
|
||||
@patch("app.library.Tasks.Scheduler")
|
||||
@patch("app.library.Tasks.DownloadQueue")
|
||||
@patch("app.library.Tasks.Services")
|
||||
async def test_inspect_static_only_with_no_handler_match(
|
||||
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
||||
):
|
||||
"""Test inspect with static_only=True when no handler matches."""
|
||||
from app.library.Tasks import TaskFailure
|
||||
|
||||
mock_config_instance = Mock(
|
||||
debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *"
|
||||
)
|
||||
mock_config.get_instance.return_value = mock_config_instance
|
||||
mock_eventbus.get_instance.return_value = Mock()
|
||||
mock_scheduler.get_instance.return_value = Mock()
|
||||
mock_download_queue.get_instance.return_value = Mock()
|
||||
|
||||
# Mock Services to simulate failed can_handle
|
||||
mock_services_instance = Mock()
|
||||
mock_services_instance.handle_sync = Mock(return_value=False)
|
||||
mock_services.get_instance.return_value = mock_services_instance
|
||||
|
||||
# Create a mock handler that won't match
|
||||
mock_handler = Mock()
|
||||
mock_handler.__name__ = "TestHandler"
|
||||
mock_handler.can_handle = Mock(return_value=False)
|
||||
|
||||
tasks = Tasks(file=None, config=mock_config_instance)
|
||||
handler = tasks.get_handler()
|
||||
handler._handlers = [mock_handler]
|
||||
|
||||
# Call inspect with static_only=True
|
||||
result = await handler.inspect(
|
||||
url="https://unsupported.com/feed", preset="default", handler_name=None, static_only=True
|
||||
)
|
||||
|
||||
# Verify result is a TaskFailure
|
||||
assert isinstance(result, TaskFailure)
|
||||
assert result.metadata["matched"] is False
|
||||
assert result.metadata["handler"] is None
|
||||
|
|
|
|||
|
|
@ -170,9 +170,13 @@
|
|||
</span>
|
||||
</span>
|
||||
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid fa-rss" /></span>
|
||||
<span>{{ item.handler_enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
<span class="icon-text is-clickable" @click="toggleHandlerEnabled(item)"
|
||||
v-tooltip="'Click to ' + (item.handler_enabled !== false ? 'disable' : 'enable') + ' task handler'">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-rss"
|
||||
:class="{ 'has-text-success': item.handler_enabled !== false, 'has-text-danger': item.handler_enabled === false }" />
|
||||
</span>
|
||||
<span>{{ item.handler_enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</span>
|
||||
|
||||
<span class="icon-text" v-if="item.preset">
|
||||
|
|
@ -185,9 +189,13 @@
|
|||
<span v-if="item.timer" class="has-tooltip" v-tooltip="item.timer">
|
||||
{{ tryParse(item.timer) }}
|
||||
</span>
|
||||
<span v-else class="has-text-danger">
|
||||
<span v-else-if="!willTaskBeProcessed(item)" class="has-text-danger">
|
||||
<span class="icon"> <i class="fa-solid fa-exclamation" /> </span>
|
||||
No timer is set
|
||||
No timer or handler
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="icon"> <i class="fa-solid fa-rss" /> </span>
|
||||
Handler only
|
||||
</span>
|
||||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
|
|
@ -278,10 +286,11 @@
|
|||
:class="{ 'fa-circle-pause has-text-success': item.auto_start, 'fa-circle-play has-text-danger': !item.auto_start }" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<span class="icon" v-tooltip="`Task handler is ${item.handler_enabled ? 'enabled' : 'disabled'}`">
|
||||
<div class="control is-clickable" @click="toggleHandlerEnabled(item)">
|
||||
<span class="icon"
|
||||
v-tooltip="`Task handler is ${item.handler_enabled !== false ? 'enabled' : 'disabled'}. Click to toggle.`">
|
||||
<i class="fa-solid fa-rss"
|
||||
:class="{ 'has-text-success': item.handler_enabled, 'has-text-danger': !item.handler_enabled }" />
|
||||
:class="{ 'has-text-success': item.handler_enabled !== false, 'has-text-danger': item.handler_enabled === false }" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="control is-clickable" @click="toggleEnabled(item)">
|
||||
|
|
@ -308,15 +317,19 @@
|
|||
<div class="content">
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon">
|
||||
<i class="fa-solid"
|
||||
:class="{ 'fa-clock': item.timer, 'has-text-danger fa-exclamation': !item.timer }" />
|
||||
<i class="fa-solid" :class="{
|
||||
'fa-clock': item.timer,
|
||||
'fa-rss': !item.timer && willTaskBeProcessed(item),
|
||||
'has-text-danger fa-exclamation': !willTaskBeProcessed(item)
|
||||
}" />
|
||||
</span>
|
||||
<span v-if="item.timer">
|
||||
<NuxtLink target="_blank" :to="`https://crontab.guru/#${item.timer.replace(/ /g, '_')}`">
|
||||
{{ item.timer }} - {{ tryParse(item.timer) }}
|
||||
</NuxtLink>
|
||||
</span>
|
||||
<span class="has-text-danger" v-else>No timer is set</span>
|
||||
<span v-else-if="willTaskBeProcessed(item)">Handler only</span>
|
||||
<span v-else class="has-text-danger">No timer or handler</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
|
|
@ -458,12 +471,14 @@ import { useConfirm } from '~/composables/useConfirm'
|
|||
import TaskInspect from '~/components/TaskInspect.vue'
|
||||
import type { task_item, exported_task, error_response } from '~/types/tasks'
|
||||
import { sleep } from '~/utils'
|
||||
import { useSessionCache } from '~/utils/cache'
|
||||
|
||||
const box = useConfirm()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const { confirmDialog: cDialog } = useDialog()
|
||||
const sessionCache = useSessionCache()
|
||||
const display_style = useStorage<string>("tasks_display_style", "cards")
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 })
|
||||
|
||||
|
|
@ -527,6 +542,66 @@ watch(() => socket.isConnected, async () => {
|
|||
}
|
||||
})
|
||||
|
||||
const CACHE_KEY = 'tasks:handler_support'
|
||||
const taskHandlerSupport = ref<Record<string, boolean>>(sessionCache.get(CACHE_KEY) || {})
|
||||
watch(taskHandlerSupport, (newValue) => sessionCache.set(CACHE_KEY, newValue), { deep: true })
|
||||
|
||||
const getCacheKey = (task: task_item): string => `${task.id}:${task.url}`
|
||||
|
||||
const cleanStaleCache = (currentTasks: Array<task_item>) => {
|
||||
const validKeys = new Set(currentTasks.map(task => getCacheKey(task)))
|
||||
const cacheKeys = Object.keys(taskHandlerSupport.value)
|
||||
|
||||
for (const key of cacheKeys) {
|
||||
if (!validKeys.has(key)) {
|
||||
taskHandlerSupport.value[key] = undefined as any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recheckHandlerSupport = async (updatedTasks: Array<task_item>) => {
|
||||
for (const task of updatedTasks) {
|
||||
if (!task.timer && false !== task.handler_enabled) {
|
||||
await checkHandlerSupport(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkHandlerSupport = async (task: task_item): Promise<boolean> => {
|
||||
const cacheKey = getCacheKey(task)
|
||||
|
||||
if (undefined !== taskHandlerSupport.value[cacheKey]) {
|
||||
return taskHandlerSupport.value[cacheKey] as boolean
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request('/api/tasks/inspect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: task.url, static_only: true })
|
||||
})
|
||||
const data = await response.json()
|
||||
const supported = true === data?.matched
|
||||
taskHandlerSupport.value[cacheKey] = supported
|
||||
return supported
|
||||
} catch {
|
||||
taskHandlerSupport.value[cacheKey] = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const willTaskBeProcessed = (task: task_item): boolean => {
|
||||
if (false === task.enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasTimer = !!(task.timer && task.timer.trim())
|
||||
const cacheKey = getCacheKey(task)
|
||||
const hasHandler = false !== task.handler_enabled && true === taskHandlerSupport.value[cacheKey]
|
||||
|
||||
return hasTimer || hasHandler
|
||||
}
|
||||
|
||||
const filteredTasks = computed<task_item[]>(() => {
|
||||
const q = query.value?.toLowerCase();
|
||||
if (!q) return tasks.value;
|
||||
|
|
@ -551,6 +626,8 @@ const reloadContent = async (fromMounted: boolean = false) => {
|
|||
}
|
||||
|
||||
tasks.value = data
|
||||
cleanStaleCache(data)
|
||||
await recheckHandlerSupport(data)
|
||||
} catch (e) {
|
||||
if (fromMounted) {
|
||||
return
|
||||
|
|
@ -602,6 +679,8 @@ const updateTasks = async (items: Array<task_item>) => {
|
|||
}
|
||||
|
||||
tasks.value = data
|
||||
cleanStaleCache(data)
|
||||
await recheckHandlerSupport(data)
|
||||
resetForm(true)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
|
|
@ -686,7 +765,11 @@ const toggleEnabled = async (item: task_item) => {
|
|||
const actionText = newStatus ? 'enable' : 'disable'
|
||||
|
||||
try {
|
||||
const response = await request(`/api/tasks/${item.id}/toggle`, { method: 'POST' })
|
||||
const response = await request(`/api/tasks/${item.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: newStatus })
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (data?.error) {
|
||||
|
|
@ -702,6 +785,31 @@ const toggleEnabled = async (item: task_item) => {
|
|||
}
|
||||
}
|
||||
|
||||
const toggleHandlerEnabled = async (item: task_item) => {
|
||||
const newStatus = !item.handler_enabled
|
||||
const actionText = newStatus ? 'enable' : 'disable'
|
||||
|
||||
try {
|
||||
const response = await request(`/api/tasks/${item.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ handler_enabled: newStatus })
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (data?.error) {
|
||||
toast.error(data.error)
|
||||
return
|
||||
}
|
||||
|
||||
item.handler_enabled = data.handler_enabled
|
||||
const func = data.handler_enabled ? 'success' : 'warning'
|
||||
toast[func](`Task handler for '${item.name}' ${data.handler_enabled ? 'enabled' : 'disabled'}.`)
|
||||
} catch (e: any) {
|
||||
toast.error(`Failed to ${actionText} task handler. ${e.message || 'Unknown error.'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const updateItem = async ({ reference, task, archive_all }: { reference?: string | null | undefined, task: task_item, archive_all?: boolean }) => {
|
||||
if (reference) {
|
||||
// -- find the task index.
|
||||
|
|
|
|||
Loading…
Reference in a new issue