Skip to content

Commit 4730c46

Browse files
feat: add dashboard base path (#119)
* feat: add reverse proxy support and basePath configuration for the dashboard * feat: add dashboard base path and test job functionality
1 parent 4bccde6 commit 4730c46

15 files changed

Lines changed: 238 additions & 42 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { logger, Sidequest } from "sidequest";
2+
import { TestJob } from "./test-job.js";
3+
4+
async function main() {
5+
logger().info("Starting Sidequest with dashboard base path...");
6+
7+
await Sidequest.start({
8+
backend: {
9+
driver: "@sidequest/sqlite-backend",
10+
config: "./sidequest.sqlite",
11+
},
12+
dashboard: {
13+
enabled: true,
14+
port: 8678,
15+
basePath: "/admin/sidequest",
16+
},
17+
queues: [{ name: "default", concurrency: 1 }],
18+
});
19+
20+
logger().info("\n✅ Sidequest started!");
21+
logger().info("🌐 Dashboard should be accessible at: http://localhost:8678/admin/sidequest");
22+
logger().info("\n📝 Testing URLs:");
23+
logger().info(" - Dashboard: http://localhost:8678/admin/sidequest/");
24+
logger().info(" - Jobs: http://localhost:8678/admin/sidequest/jobs");
25+
logger().info(" - Queues: http://localhost:8678/admin/sidequest/queues");
26+
logger().info(" - Logo: http://localhost:8678/admin/sidequest/public/img/logo.png");
27+
logger().info(" - Styles: http://localhost:8678/admin/sidequest/public/css/styles.css");
28+
29+
// Enqueue some test jobs
30+
logger().info("\n📦 Enqueueing test jobs...");
31+
for (let i = 1; i <= 5; i++) {
32+
await Sidequest.build(TestJob).enqueue();
33+
logger().info(` ✓ Job ${i} enqueued`);
34+
}
35+
36+
logger().info("\n⚠️ Note: The dashboard should NOT be accessible at http://localhost:8678/ (without base path)");
37+
logger().info("💡 Try accessing the dashboard and verify:");
38+
logger().info(" 1. All assets load correctly (logo, styles, scripts)");
39+
logger().info(" 2. Navigation links work (Dashboard, Jobs, Queues)");
40+
logger().info(" 3. Job actions work (run, cancel, rerun)");
41+
logger().info(" 4. HTMX polling/updates work correctly");
42+
logger().info("\n🛑 Press Ctrl+C to stop\n");
43+
}
44+
45+
// eslint-disable-next-line no-console
46+
main().catch(console.error);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Job, logger } from "sidequest";
2+
3+
export class TestJob extends Job {
4+
async run() {
5+
logger().info(`Processing test job: ${this.id}`);
6+
await new Promise((resolve) => setTimeout(resolve, 1000));
7+
return { message: "Job completed successfully" };
8+
}
9+
}

packages/dashboard/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,32 @@ await dashboard.start({
8282
// Dashboard available at http://localhost:8678
8383
```
8484

85+
### Reverse Proxy Setup
86+
87+
When deploying behind a reverse proxy, use the `basePath` option:
88+
89+
```typescript
90+
await Sidequest.start({
91+
dashboard: {
92+
port: 8678,
93+
basePath: "/admin/sidequest", // Serve at /admin/sidequest
94+
auth: {
95+
user: "admin",
96+
password: "secure-password",
97+
},
98+
},
99+
});
100+
```
101+
102+
Then configure your reverse proxy to forward requests:
103+
104+
```nginx
105+
# Nginx example
106+
location /admin/sidequest/ {
107+
proxy_pass http://localhost:8678/admin/sidequest/;
108+
}
109+
```
110+
85111
## License
86112

87113
LGPL-3.0-or-later

packages/dashboard/src/config.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
11
import { BackendConfig } from "@sidequest/backend";
22

3+
/**
4+
* Configuration interface for the Sidequest dashboard.
5+
*
6+
* Defines the available options for configuring the dashboard including
7+
* backend connectivity, server settings, authentication, and routing.
8+
*
9+
* @interface DashboardConfig
10+
* @example
11+
* ```typescript
12+
* const config: DashboardConfig = {
13+
* enabled: true,
14+
* port: 3000,
15+
* basePath: "/admin/sidequest",
16+
* auth: {
17+
* user: "admin",
18+
* password: "secure-password"
19+
* }
20+
* };
21+
* ```
22+
*/
323
export interface DashboardConfig {
24+
/**
25+
* Configuration for connecting to the Sidequest backend.
26+
* This includes the driver and any necessary connection options.
27+
*/
428
backendConfig?: BackendConfig;
29+
/**
30+
* Indicates whether the dashboard is enabled.
31+
* If set to false, the dashboard server will not start.
32+
*
33+
* @default false
34+
*/
535
enabled?: boolean;
36+
/**
37+
* Port number on which the dashboard server will listen for incoming requests.
38+
*
39+
* @default 8678
40+
*/
641
port?: number;
42+
/**
43+
* Base path for the dashboard when served behind a reverse proxy.
44+
* For example, if you want to serve the dashboard at `/admin/sidequest`,
45+
* set this to `/admin/sidequest`.
46+
*
47+
* @example "/admin/sidequest"
48+
* @default ""
49+
*/
50+
basePath?: string;
51+
/**
52+
* Optional basic authentication configuration.
53+
* If provided, the dashboard will require users to authenticate
54+
* using the specified username and password.
55+
*
56+
* @example
57+
* ```typescript
58+
* auth: {
59+
* user: 'admin',
60+
* password: 'secure-password'
61+
* }
62+
* ```
63+
* @default undefined
64+
*/
765
auth?: {
866
user: string;
967
password: string;

packages/dashboard/src/index.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ export class SidequestDashboard {
9696
...config,
9797
};
9898

99+
// Normalize basePath: remove trailing slash, ensure leading slash
100+
if (this.config.basePath) {
101+
this.config.basePath = this.config.basePath.replace(/\/$/, "");
102+
if (!this.config.basePath.startsWith("/")) {
103+
this.config.basePath = "/" + this.config.basePath;
104+
}
105+
} else {
106+
this.config.basePath = "";
107+
}
108+
99109
if (!this.config.enabled) {
100110
logger("Dashboard").debug(`Dashboard is disabled`);
101111
return;
@@ -128,6 +138,12 @@ export class SidequestDashboard {
128138
if (logger().isDebugEnabled()) {
129139
this.app?.use(morgan("combined"));
130140
}
141+
142+
// Make basePath available to all templates
143+
this.app?.use((req, res, next) => {
144+
res.locals.basePath = this.config!.basePath ?? "";
145+
next();
146+
});
131147
}
132148

133149
/**
@@ -180,7 +196,8 @@ export class SidequestDashboard {
180196
this.app!.set("view engine", "ejs");
181197
this.app!.set("views", path.join(import.meta.dirname, "views"));
182198
this.app!.set("layout", path.join(import.meta.dirname, "views", "layout"));
183-
this.app!.use("/public", express.static(path.join(import.meta.dirname, "public")));
199+
const publicPath = this.config!.basePath ? `${this.config!.basePath}/public` : "/public";
200+
this.app!.use(publicPath, express.static(path.join(import.meta.dirname, "public")));
184201
}
185202

186203
/**
@@ -195,9 +212,14 @@ export class SidequestDashboard {
195212
*/
196213
setupRoutes() {
197214
logger("Dashboard").debug(`Setting up routes`);
198-
this.app!.use(...createDashboardRouter(this.backend!));
199-
this.app!.use(...createJobsRouter(this.backend!));
200-
this.app!.use(...createQueuesRouter(this.backend!));
215+
const basePath = this.config!.basePath ?? "";
216+
const [dashboardPath, dashboardRouter] = createDashboardRouter(this.backend!);
217+
const [jobsPath, jobsRouter] = createJobsRouter(this.backend!);
218+
const [queuesPath, queuesRouter] = createQueuesRouter(this.backend!);
219+
220+
this.app!.use(basePath + dashboardPath, dashboardRouter);
221+
this.app!.use(basePath + jobsPath, jobsRouter);
222+
this.app!.use(basePath + queuesPath, queuesRouter);
201223
}
202224

203225
/**

packages/dashboard/src/public/js/dashboard.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ const jobsTimeline = new Chart(ctx, {
5959
});
6060

6161
async function refreshGraph() {
62-
const res = await fetch(`/dashboard/graph-data?range=${currentRange}`);
62+
const basePath = window.SIDEQUEST_BASE_PATH || "";
63+
const res = await fetch(`${basePath}/dashboard/graph-data?range=${currentRange}`);
6364
const graph = await res.json();
6465
const timestamps = graph.map((entry) => entry.timestamp);
6566

packages/dashboard/src/views/layout.ejs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,40 @@
44
<meta charset="UTF-8" />
55
<title><%= title || 'Sidequest Dashboard' %></title>
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<link href="/public/css/styles.css" rel="stylesheet" />
8-
<script src="/public/js/htmx.js"></script>
9-
<script src="/public/js/feather-icons.js"></script>
10-
<script src="/public/js/highlight.js"></script>
11-
<script src="/public/js/chart.js"></script>
12-
<script src="/public/js/scroll.js"></script>
13-
<script src="/public/js/selection.js"></script>
7+
<link href="<%= basePath %>/public/css/styles.css" rel="stylesheet" />
8+
<script src="<%= basePath %>/public/js/htmx.js"></script>
9+
<script src="<%= basePath %>/public/js/feather-icons.js"></script>
10+
<script src="<%= basePath %>/public/js/highlight.js"></script>
11+
<script src="<%= basePath %>/public/js/chart.js"></script>
12+
<script src="<%= basePath %>/public/js/scroll.js"></script>
13+
<script src="<%= basePath %>/public/js/selection.js"></script>
1414
</head>
1515
<body class="h-full bg-base-100 text-base-content">
1616
<div class="flex h-full">
1717
<!-- Sidebar -->
1818
<aside class="w-64 bg-neutral border-r border-base-100 flex flex-col">
1919
<div class="p-4 flex items-center gap-2 border-b border-base-100">
20-
<img src="/public/img/logo.png" alt="Sidequest Logo" class="h-14 w-auto" />
20+
<img src="<%= basePath %>/public/img/logo.png" alt="Sidequest Logo" class="h-14 w-auto" />
2121
<span class="text-xl font-semibold text-white">Sidequest</span>
2222
</div>
2323
<nav class="flex-1 p-4 space-y-2 text-sm">
24-
<a href="/" class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content">Dashboard</a>
25-
<a href="/jobs" class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content">Jobs</a>
26-
<a href="/queues" class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content">Queues</a>
24+
<a
25+
href="<%= basePath %>/"
26+
class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content"
27+
>Dashboard</a
28+
>
29+
<a
30+
href="<%= basePath %>/jobs"
31+
class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content"
32+
>Jobs</a
33+
>
34+
<a
35+
href="<%= basePath %>/queues"
36+
class="btn btn-ghost btn-sm justify-start w-full text-base-content hover:bg-secondary hover:text-secondary-content"
37+
>Queues</a
38+
>
2739
</nav>
28-
<div class="p-4 border-t border-base-100 text-xs text-neutral-content">
29-
v0.1.0 — OSS Edition
30-
</div>
40+
<div class="p-4 border-t border-base-100 text-xs text-neutral-content">v0.1.0 — OSS Edition</div>
3141
</aside>
3242

3343
<!-- Main content -->

packages/dashboard/src/views/pages/index.ejs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@
1919
</div>
2020
</div>
2121

22-
<script src="/public/js/dashboard.js"></script>
22+
<script>
23+
window.SIDEQUEST_BASE_PATH = "<%= basePath %>";
24+
</script>
25+
<script src="<%= basePath %>/public/js/dashboard.js"></script>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<%- include('../partials/job-view', { job }) %>
22

3-
<script src="/public/js/job.js"></script>
3+
<script src="<%= basePath %>/public/js/job.js"></script>

packages/dashboard/src/views/pages/jobs.ejs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<section class="space-y-6">
2-
<form method="GET" id="filter-form" hx-get="/jobs" hx-target="#jobs-table"
2+
<form method="GET" id="filter-form" hx-get="<%= basePath %>/jobs" hx-target="#jobs-table"
33
hx-trigger="change from:* delay:300ms, every 3s, jobChanged" hx-push-url="true" hx-sync="this:replace">
44
<div class="flex flex-wrap items-end gap-4 text-base mb-4">
55
<div class="form-control w-48">
@@ -75,7 +75,7 @@
7575
</form>
7676
</section>
7777

78-
<script src="/public/js/jobs.js"></script>
78+
<script src="<%= basePath %>/public/js/jobs.js"></script>
7979

8080
<script>
8181
const startUtc = "<%= filters.start %>";

0 commit comments

Comments
 (0)