diff --git a/examples/06-dashboard-base-path/index.ts b/examples/06-dashboard-base-path/index.ts new file mode 100644 index 00000000..4daebebc --- /dev/null +++ b/examples/06-dashboard-base-path/index.ts @@ -0,0 +1,46 @@ +import { logger, Sidequest } from "sidequest"; +import { TestJob } from "./test-job.js"; + +async function main() { + logger().info("Starting Sidequest with dashboard base path..."); + + await Sidequest.start({ + backend: { + driver: "@sidequest/sqlite-backend", + config: "./sidequest.sqlite", + }, + dashboard: { + enabled: true, + port: 8678, + basePath: "/admin/sidequest", + }, + queues: [{ name: "default", concurrency: 1 }], + }); + + logger().info("\nโœ… Sidequest started!"); + logger().info("๐ŸŒ Dashboard should be accessible at: http://localhost:8678/admin/sidequest"); + logger().info("\n๐Ÿ“ Testing URLs:"); + logger().info(" - Dashboard: http://localhost:8678/admin/sidequest/"); + logger().info(" - Jobs: http://localhost:8678/admin/sidequest/jobs"); + logger().info(" - Queues: http://localhost:8678/admin/sidequest/queues"); + logger().info(" - Logo: http://localhost:8678/admin/sidequest/public/img/logo.png"); + logger().info(" - Styles: http://localhost:8678/admin/sidequest/public/css/styles.css"); + + // Enqueue some test jobs + logger().info("\n๐Ÿ“ฆ Enqueueing test jobs..."); + for (let i = 1; i <= 5; i++) { + await Sidequest.build(TestJob).enqueue(); + logger().info(` โœ“ Job ${i} enqueued`); + } + + logger().info("\nโš ๏ธ Note: The dashboard should NOT be accessible at http://localhost:8678/ (without base path)"); + logger().info("๐Ÿ’ก Try accessing the dashboard and verify:"); + logger().info(" 1. All assets load correctly (logo, styles, scripts)"); + logger().info(" 2. Navigation links work (Dashboard, Jobs, Queues)"); + logger().info(" 3. Job actions work (run, cancel, rerun)"); + logger().info(" 4. HTMX polling/updates work correctly"); + logger().info("\n๐Ÿ›‘ Press Ctrl+C to stop\n"); +} + +// eslint-disable-next-line no-console +main().catch(console.error); diff --git a/examples/06-dashboard-base-path/test-job.ts b/examples/06-dashboard-base-path/test-job.ts new file mode 100644 index 00000000..c4e02fce --- /dev/null +++ b/examples/06-dashboard-base-path/test-job.ts @@ -0,0 +1,9 @@ +import { Job, logger } from "sidequest"; + +export class TestJob extends Job { + async run() { + logger().info(`Processing test job: ${this.id}`); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return { message: "Job completed successfully" }; + } +} diff --git a/packages/dashboard/README.md b/packages/dashboard/README.md index 57127d61..17fd6e6d 100644 --- a/packages/dashboard/README.md +++ b/packages/dashboard/README.md @@ -82,6 +82,32 @@ await dashboard.start({ // Dashboard available at http://localhost:8678 ``` +### Reverse Proxy Setup + +When deploying behind a reverse proxy, use the `basePath` option: + +```typescript +await Sidequest.start({ + dashboard: { + port: 8678, + basePath: "/admin/sidequest", // Serve at /admin/sidequest + auth: { + user: "admin", + password: "secure-password", + }, + }, +}); +``` + +Then configure your reverse proxy to forward requests: + +```nginx +# Nginx example +location /admin/sidequest/ { + proxy_pass http://localhost:8678/admin/sidequest/; +} +``` + ## License LGPL-3.0-or-later diff --git a/packages/dashboard/src/config.ts b/packages/dashboard/src/config.ts index e28ad058..d48cc570 100644 --- a/packages/dashboard/src/config.ts +++ b/packages/dashboard/src/config.ts @@ -1,9 +1,67 @@ import { BackendConfig } from "@sidequest/backend"; +/** + * Configuration interface for the Sidequest dashboard. + * + * Defines the available options for configuring the dashboard including + * backend connectivity, server settings, authentication, and routing. + * + * @interface DashboardConfig + * @example + * ```typescript + * const config: DashboardConfig = { + * enabled: true, + * port: 3000, + * basePath: "/admin/sidequest", + * auth: { + * user: "admin", + * password: "secure-password" + * } + * }; + * ``` + */ export interface DashboardConfig { + /** + * Configuration for connecting to the Sidequest backend. + * This includes the driver and any necessary connection options. + */ backendConfig?: BackendConfig; + /** + * Indicates whether the dashboard is enabled. + * If set to false, the dashboard server will not start. + * + * @default false + */ enabled?: boolean; + /** + * Port number on which the dashboard server will listen for incoming requests. + * + * @default 8678 + */ port?: number; + /** + * Base path for the dashboard when served behind a reverse proxy. + * For example, if you want to serve the dashboard at `/admin/sidequest`, + * set this to `/admin/sidequest`. + * + * @example "/admin/sidequest" + * @default "" + */ + basePath?: string; + /** + * Optional basic authentication configuration. + * If provided, the dashboard will require users to authenticate + * using the specified username and password. + * + * @example + * ```typescript + * auth: { + * user: 'admin', + * password: 'secure-password' + * } + * ``` + * @default undefined + */ auth?: { user: string; password: string; diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index 9b448f30..bb950d39 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -96,6 +96,16 @@ export class SidequestDashboard { ...config, }; + // Normalize basePath: remove trailing slash, ensure leading slash + if (this.config.basePath) { + this.config.basePath = this.config.basePath.replace(/\/$/, ""); + if (!this.config.basePath.startsWith("/")) { + this.config.basePath = "/" + this.config.basePath; + } + } else { + this.config.basePath = ""; + } + if (!this.config.enabled) { logger("Dashboard").debug(`Dashboard is disabled`); return; @@ -128,6 +138,12 @@ export class SidequestDashboard { if (logger().isDebugEnabled()) { this.app?.use(morgan("combined")); } + + // Make basePath available to all templates + this.app?.use((req, res, next) => { + res.locals.basePath = this.config!.basePath ?? ""; + next(); + }); } /** @@ -180,7 +196,8 @@ export class SidequestDashboard { this.app!.set("view engine", "ejs"); this.app!.set("views", path.join(import.meta.dirname, "views")); this.app!.set("layout", path.join(import.meta.dirname, "views", "layout")); - this.app!.use("/public", express.static(path.join(import.meta.dirname, "public"))); + const publicPath = this.config!.basePath ? `${this.config!.basePath}/public` : "/public"; + this.app!.use(publicPath, express.static(path.join(import.meta.dirname, "public"))); } /** @@ -195,9 +212,14 @@ export class SidequestDashboard { */ setupRoutes() { logger("Dashboard").debug(`Setting up routes`); - this.app!.use(...createDashboardRouter(this.backend!)); - this.app!.use(...createJobsRouter(this.backend!)); - this.app!.use(...createQueuesRouter(this.backend!)); + const basePath = this.config!.basePath ?? ""; + const [dashboardPath, dashboardRouter] = createDashboardRouter(this.backend!); + const [jobsPath, jobsRouter] = createJobsRouter(this.backend!); + const [queuesPath, queuesRouter] = createQueuesRouter(this.backend!); + + this.app!.use(basePath + dashboardPath, dashboardRouter); + this.app!.use(basePath + jobsPath, jobsRouter); + this.app!.use(basePath + queuesPath, queuesRouter); } /** diff --git a/packages/dashboard/src/public/js/dashboard.js b/packages/dashboard/src/public/js/dashboard.js index 44167cb6..a4be925d 100644 --- a/packages/dashboard/src/public/js/dashboard.js +++ b/packages/dashboard/src/public/js/dashboard.js @@ -59,7 +59,8 @@ const jobsTimeline = new Chart(ctx, { }); async function refreshGraph() { - const res = await fetch(`/dashboard/graph-data?range=${currentRange}`); + const basePath = window.SIDEQUEST_BASE_PATH || ""; + const res = await fetch(`${basePath}/dashboard/graph-data?range=${currentRange}`); const graph = await res.json(); const timestamps = graph.map((entry) => entry.timestamp); diff --git a/packages/dashboard/src/views/layout.ejs b/packages/dashboard/src/views/layout.ejs index 0147ffe4..18a8282e 100644 --- a/packages/dashboard/src/views/layout.ejs +++ b/packages/dashboard/src/views/layout.ejs @@ -4,30 +4,40 @@ <%= title || 'Sidequest Dashboard' %> - - - - - - - + + + + + + +
diff --git a/packages/dashboard/src/views/pages/index.ejs b/packages/dashboard/src/views/pages/index.ejs index 9f2907a4..3c135862 100644 --- a/packages/dashboard/src/views/pages/index.ejs +++ b/packages/dashboard/src/views/pages/index.ejs @@ -19,4 +19,7 @@
- + + diff --git a/packages/dashboard/src/views/pages/job.ejs b/packages/dashboard/src/views/pages/job.ejs index f2e4ee71..f72bfc3a 100644 --- a/packages/dashboard/src/views/pages/job.ejs +++ b/packages/dashboard/src/views/pages/job.ejs @@ -1,3 +1,3 @@ <%- include('../partials/job-view', { job }) %> - + diff --git a/packages/dashboard/src/views/pages/jobs.ejs b/packages/dashboard/src/views/pages/jobs.ejs index 44936ffa..e9b8ced2 100644 --- a/packages/dashboard/src/views/pages/jobs.ejs +++ b/packages/dashboard/src/views/pages/jobs.ejs @@ -1,5 +1,5 @@
-
@@ -75,7 +75,7 @@
- +