diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 470aa13..b9ce468 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -46,6 +46,8 @@
"userLabel": "User Label",
"userLabelDescription": "Label to apply to users created with this invite.",
"logs": "Logs",
+ "tasks": "Tasks",
+ "tasksDescription": "Tasks are large actions that may be run periodically in the background. You can manually trigger them here if you wish.",
"announce": "Announce",
"templates": "Templates",
"subject": "Subject",
@@ -58,6 +60,7 @@
"unlink": "Unlink Account",
"deleted": "Deleted",
"disabled": "Disabled",
+ "run": "Run",
"sendPWR": "Send Password Reset",
"noResultsFound": "No Results Found",
"noResultsFoundLocally": "Only loaded records were searched. You can load more, or perform the search over all records on the server.",
@@ -264,7 +267,8 @@
"errorInvalidDate": "Date is invalid.",
"errorInvalidJSON": "Invalid JSON.",
"updateAvailable": "A new update is available, check settings.",
- "noUpdatesAvailable": "No new updates available."
+ "noUpdatesAvailable": "No new updates available.",
+ "runTask": "Triggered task."
},
"quantityStrings": {
"modifySettingsFor": {
diff --git a/models.go b/models.go
index 4cceaa8..8edf78a 100644
--- a/models.go
+++ b/models.go
@@ -492,3 +492,13 @@ type PagePathsDTO struct {
// The subdirectory the app is meant to be accessed from ("Reverse proxy subfolder")
TrueBase string `json:"TrueBase"`
}
+
+type TasksDTO struct {
+ Tasks []TaskDTO `json:"tasks"`
+}
+
+type TaskDTO struct {
+ URL string `json:"url"`
+ Name string `json:"name"`
+ Description string ` json:"description"`
+}
diff --git a/router.go b/router.go
index 8779e16..8befeab 100644
--- a/router.go
+++ b/router.go
@@ -244,6 +244,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
api.GET(p+"/logs", app.GetLog)
+ api.GET(p+"/tasks", app.TaskList)
api.POST(p+"/tasks/housekeeping", app.TaskHousekeeping)
api.POST(p+"/tasks/users", app.TaskUserCleanup)
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
diff --git a/tasks.go b/tasks.go
index 4a823c8..45cda74 100644
--- a/tasks.go
+++ b/tasks.go
@@ -7,6 +7,35 @@ import (
"github.com/gin-gonic/gin"
)
+// @Summary List existing task routes, with friendly names and descriptions.
+// @Produce json
+// @Success 200 {object} TasksDTO
+// @Router /tasks [get]
+// @Security Bearer
+// @tags Tasks
+func (app *appContext) TaskList(gc *gin.Context) {
+ resp := TasksDTO{Tasks: []TaskDTO{
+ TaskDTO{
+ URL: "/tasks/housekeeping",
+ Name: "Housekeeping",
+ Description: "General housekeeping tasks: Clearing expired invites & activities, unused contact details & captchas, etc.",
+ },
+ TaskDTO{
+ URL: "/tasks/users",
+ Name: "Users",
+ Description: "Checks for (pending) account expiries and performs the appropriate actions.",
+ },
+ }}
+ if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
+ resp.Tasks = append(resp.Tasks, TaskDTO{
+ URL: "/tasks/jellyseerr",
+ Name: "Jellyseerr user import",
+ Description: "Imports existing users into jellyfin and synchronizes contact details. Should only need to be run once when the feature is enabled, which is done automatically.",
+ })
+ }
+ gc.JSON(200, resp)
+}
+
// @Summary Triggers general housekeeping tasks: Clearing expired invites, activities, unused contact details, captchas, etc.
// @Success 204
// @Router /tasks/housekeeping [post]
diff --git a/ts/admin.ts b/ts/admin.ts
index ff3e5ca..6ce84d6 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -74,6 +74,8 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
window.modals.logs = new Modal(document.getElementById("modal-logs"));
+
+ window.modals.tasks = new Modal(document.getElementById("modal-tasks"));
window.modals.backedUp = new Modal(document.getElementById("modal-backed-up"));
diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts
index 9ece496..dfcd46b 100644
--- a/ts/modules/settings.ts
+++ b/ts/modules/settings.ts
@@ -936,14 +936,17 @@ export class settingsList {
private _settings: Settings;
private _advanced: boolean = false;
- private _searchbox: HTMLInputElement = document.getElementById("settings-search") as HTMLInputElement;
- private _clearSearchboxButtons: Array
= Array.from(document.getElementsByClassName("settings-search-clear")) as Array;
+ private _searchbox = document.getElementById("settings-search") as HTMLInputElement;
+ private _clearSearchboxButtons = Array.from(document.getElementsByClassName("settings-search-clear")) as Array;
private _noResultsPanel: HTMLElement = document.getElementById("settings-not-found");
private _backupSortDirection = document.getElementById("settings-backups-sort-direction") as HTMLButtonElement;
private _backupSortAscending = true;
+ private _tasksList: TasksList;
+ private _tasksButton = document.getElementById("settings-tasks") as HTMLButtonElement;
+
// Must be called -after- all section have been added.
// Takes all groups at once since members might contain each other.
addGroups = (groups: Group[]) => {
@@ -1160,6 +1163,9 @@ export class settingsList {
this._backup();
};
+ this._tasksList = new TasksList();
+ this._tasksButton.onclick = this._tasksList.load;
+
document.addEventListener("settings-show-panel", (event: CustomEvent) => {
this._showPanel(event.detail as string);
});
@@ -1187,17 +1193,21 @@ export class settingsList {
advancedEnableToggle.onchange = () => {
document.dispatchEvent(new CustomEvent("settings-advancedState", { detail: advancedEnableToggle.checked }));
+ };
+ document.addEventListener("settings-advancedState", () => {
const parent = advancedEnableToggle.parentElement;
this._advanced = advancedEnableToggle.checked;
if (this._advanced) {
parent.classList.add("~urge");
parent.classList.remove("~neutral");
+ this._tasksButton.classList.remove("unfocused");
} else {
parent.classList.add("~neutral");
parent.classList.remove("~urge");
+ this._tasksButton.classList.add("unfocused");
}
this._searchbox.oninput(null);
- };
+ });
advancedEnableToggle.checked = false;
this._searchbox.oninput = () => {
@@ -1746,3 +1756,58 @@ class MessageEditor {
};
}
}
+
+class TasksList {
+ private _list: HTMLElement = document.getElementById("modal-tasks-list");
+
+ load = () => _get("/tasks", null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4) return;
+ if (req.status != 200) return;
+ let resp = req.response["tasks"] as TaskDTO[];
+ this._list.textContent = "";
+ for (let t of resp) {
+ const task = new Task(t);
+ this._list.appendChild(task.asElement());
+ }
+ window.modals.tasks.show();
+ });
+}
+
+interface TaskDTO {
+ url: string;
+ name: string;
+ description: string;
+}
+
+class Task {
+ private _el: HTMLElement;
+ asElement = () => { return this._el };
+ constructor(t: TaskDTO) {
+ this._el = document.createElement("div");
+ this._el.classList.add("aside", "flex", "flex-row", "gap-4", "justify-between", "dark:shadow-md")
+ this._el.innerHTML = `
+
+
+
${t.name}
+ ${t.url}
+
+
${t.description}
+
+
+ `;
+ const button = this._el.querySelector("button") as HTMLButtonElement;
+ button.onclick = () => {
+ addLoader(button);
+ _post(t.url, null, (req: XMLHttpRequest) => {
+ if (req.readyState != 4) return;
+ removeLoader(button);
+ setTimeout(window.modals.tasks.close, 1000)
+ if (req.status != 204) {
+ window.notifications.customError("errorRunTask", window.lang.notif("errorFailureCheckLogs"));
+ return;
+ }
+ window.notifications.customSuccess("runTask", window.lang.notif("runTask"));
+ })
+ }
+ }
+}
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 565291c..e29ca13 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -123,6 +123,7 @@ declare interface Modals {
sendPWR?: Modal;
pwr?: Modal;
logs: Modal;
+ tasks: Modal;
email?: Modal;
enableReferralsUser?: Modal;
enableReferralsProfile?: Modal;