From 8d5729822dd0e2ed9c4c3d73325a0ddd49d3438e Mon Sep 17 00:00:00 2001 From: Bobban Rydh Date: Fri, 4 Sep 2026 20:27:05 +0200 Subject: [PATCH] Add n8n workflow schedule type, Swedish date format, and Data & Privacy page - New "n8n workflow" schedule type for manually tracked tasks, plumbed through the form, filters, group labels, and API validation, same as the earlier backup/update types. - Dates (cron next-run, server last-seen) now render in Swedish standard format (YYYY-MM-DD, 24-hour clock) via a shared formatter instead of the browser's default locale. - New Data & Privacy page describing what's stored, where, how long, and how to remove it, linked from a footer shown on every page alongside a "By bobbantech" credit linking to bobbantech.com. --- server/src/db/schema.ts | 2 +- server/src/routes/tasks.ts | 2 +- web/src/App.tsx | 11 +++++++ web/src/api/client.ts | 9 +++++- web/src/components/FilterBar.tsx | 1 + web/src/components/ServerGroup.tsx | 1 + web/src/components/TaskForm.tsx | 1 + web/src/components/TaskTable.tsx | 3 +- web/src/pages/Privacy.tsx | 52 ++++++++++++++++++++++++++++++ web/src/pages/ServersAdmin.tsx | 3 +- web/src/styles.css | 48 +++++++++++++++++++++++++++ web/src/utils/date.ts | 12 +++++++ web/tsconfig.tsbuildinfo | 2 +- 13 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 web/src/pages/Privacy.tsx create mode 100644 web/src/utils/date.ts diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index 34999b9..8c6cb59 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -20,7 +20,7 @@ export const scheduledTasks = sqliteTable("scheduled_tasks", { serverId: integer("server_id") .notNull() .references(() => servers.id, { onDelete: "cascade" }), - scheduleType: text("schedule_type").notNull(), // 'cron' | 'systemd_timer' | 'docker' | 'backup' | 'update' | 'manual' + scheduleType: text("schedule_type").notNull(), // 'cron' | 'systemd_timer' | 'docker' | 'backup' | 'update' | 'n8n_workflow' | 'manual' origin: text("origin").notNull().default("agent"), // 'agent' | 'manual' — manual rows are never touched by agent sync name: text("name").notNull(), command: text("command"), diff --git a/server/src/routes/tasks.ts b/server/src/routes/tasks.ts index 4e3bc29..f7c4f5b 100644 --- a/server/src/routes/tasks.ts +++ b/server/src/routes/tasks.ts @@ -58,7 +58,7 @@ tasksRouter.get("/", async (req, res) => { const manualTaskSchema = z.object({ serverId: z.number().int(), - scheduleType: z.enum(["cron", "systemd_timer", "docker", "backup", "update", "manual"]), + scheduleType: z.enum(["cron", "systemd_timer", "docker", "backup", "update", "n8n_workflow", "manual"]), name: z.string().min(1).max(200), command: z.string().max(1000).optional(), scheduleExpression: z.string().max(200).optional(), diff --git a/web/src/App.tsx b/web/src/App.tsx index 27b2b7a..7244ad8 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4,6 +4,7 @@ import { api, type CurrentUser, UnauthorizedError } from "./api/client"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import ServersAdmin from "./pages/ServersAdmin"; +import Privacy from "./pages/Privacy"; export default function App() { const [user, setUser] = useState(undefined); @@ -47,8 +48,18 @@ export default function App() { } /> } /> + } /> + ); } diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 6ba3bb0..554e94f 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -9,7 +9,14 @@ export interface ServerRecord { lastSeenAt: string | null; } -export type ScheduleType = "cron" | "systemd_timer" | "docker" | "backup" | "update" | "manual"; +export type ScheduleType = + | "cron" + | "systemd_timer" + | "docker" + | "backup" + | "update" + | "n8n_workflow" + | "manual"; export interface TaskRecord { id: number; diff --git a/web/src/components/FilterBar.tsx b/web/src/components/FilterBar.tsx index b2ebf4e..74cfe3f 100644 --- a/web/src/components/FilterBar.tsx +++ b/web/src/components/FilterBar.tsx @@ -39,6 +39,7 @@ export default function FilterBar({ servers, filters, onChange }: Props) { + = { docker: "Docker jobs", backup: "Backups", update: "Updates", + n8n_workflow: "n8n workflows", manual: "Other", }; diff --git a/web/src/components/TaskForm.tsx b/web/src/components/TaskForm.tsx index 0c82d70..4626a09 100644 --- a/web/src/components/TaskForm.tsx +++ b/web/src/components/TaskForm.tsx @@ -7,6 +7,7 @@ const SCHEDULE_TYPE_OPTIONS: { value: ScheduleType; label: string }[] = [ { value: "systemd_timer", label: "systemd timer" }, { value: "backup", label: "Backup" }, { value: "update", label: "Update" }, + { value: "n8n_workflow", label: "n8n workflow" }, { value: "manual", label: "Other / manual" }, ]; diff --git a/web/src/components/TaskTable.tsx b/web/src/components/TaskTable.tsx index 21484df..db831fa 100644 --- a/web/src/components/TaskTable.tsx +++ b/web/src/components/TaskTable.tsx @@ -1,6 +1,7 @@ import cronstrue from "cronstrue"; import { CronExpressionParser } from "cron-parser"; import type { TaskRecord } from "../api/client"; +import { formatDateTime } from "../utils/date"; function describeSchedule(task: TaskRecord): string { if (!task.scheduleExpression) return "—"; @@ -77,7 +78,7 @@ export default function TaskTable({ tasks, onEdit, onDelete }: Props) { } > {nextRun.estimated ? "~" : ""} - {nextRun.date.toLocaleString()} + {formatDateTime(nextRun.date)} ) : ( "—" diff --git a/web/src/pages/Privacy.tsx b/web/src/pages/Privacy.tsx new file mode 100644 index 0000000..64c55af --- /dev/null +++ b/web/src/pages/Privacy.tsx @@ -0,0 +1,52 @@ +export default function Privacy() { + return ( +
+

Data & Privacy

+

+ This is a self-hosted tool with no accounts, tracking, or analytics of its own. Everything + below is stored on this server and nowhere else. +

+ +

Who you are

+

+ Sign-in is handled entirely by your Authentik instance. This app has no password or user + database of its own — it only receives your name, email, and account ID from Authentik at + login, and keeps that in your session for as long as you're signed in. +

+ +

What's stored

+
    +
  • + Servers — name, hostname, and description you enter, plus a hashed copy + of each server's agent API token (the raw token is shown once at creation and never + stored). +
  • +
  • + Scheduled tasks — name, command, and schedule for each cron job, systemd + timer, or manually added task, and which server it belongs to. +
  • +
  • + Your session — the name, email, and account ID Authentik gives us, held + only in a session file, separate from the database above. +
  • +
+

None of this is shared with any third party or leaves this server.

+ +

How long it's kept

+

+ Servers and tasks are kept until you delete them from the Servers page or + the Dashboard. A task that stops being reported by its agent is marked "stale" rather than + deleted, so a temporary agent failure doesn't erase history — you can remove it manually at + any time. Your session expires automatically after 7 days of being issued, or immediately + when you sign out. +

+ +

Removing your data

+

+ Deleting a server also deletes every task recorded under it. Signing out ends your session + immediately. Since there's no separate user account to close, that's the entirety of what + can be removed. +

+
+ ); +} diff --git a/web/src/pages/ServersAdmin.tsx b/web/src/pages/ServersAdmin.tsx index efa5514..2b46dba 100644 --- a/web/src/pages/ServersAdmin.tsx +++ b/web/src/pages/ServersAdmin.tsx @@ -1,6 +1,7 @@ import { useEffect, useState, type FormEvent } from "react"; import { api, type ServerRecord } from "../api/client"; import CopyButton from "../components/CopyButton"; +import { formatDateTime } from "../utils/date"; function installCommand(token: string): string { return `curl -fsSL ${window.location.origin}/agent/linux/install.sh | sudo API_URL=${window.location.origin} API_TOKEN=${token} bash`; @@ -144,7 +145,7 @@ export default function ServersAdmin() { {s.name} {s.hostname ?? "—"} {s.apiTokenPrefix}… - {s.lastSeenAt ? new Date(s.lastSeenAt).toLocaleString() : "never"} + {s.lastSeenAt ? formatDateTime(new Date(s.lastSeenAt)) : "never"}