@sidequest/dashboard
Version:
@sidequest/dashboard is the web dashboard for Sidequest, a distributed background job queue system.
266 lines (262 loc) • 10.2 kB
JavaScript
'use strict';
var backend = require('@sidequest/backend');
var core = require('@sidequest/core');
var express = require('express');
var basicAuth = require('express-basic-auth');
var expressLayouts = require('express-ejs-layouts');
var morgan = require('morgan');
var path = require('node:path');
var dashboard = require('./resources/dashboard.cjs');
var jobs = require('./resources/jobs.cjs');
var queues = require('./resources/queues.cjs');
/**
* A dashboard server for monitoring and managing Sidequest jobs and queues.
*
* The SidequestDashboard class provides a web-based interface for viewing job status,
* queue information, and other Sidequest-related data. It sets up an Express.js server
* with EJS templating, optional basic authentication, and connects to a configurable backend.
*
* @example
* ```typescript
* const dashboard = new SidequestDashboard();
* await dashboard.start({
* port: 3000,
* enabled: true,
* auth: {
* user: 'admin',
* password: 'secret'
* },
* backendConfig: {
* driver: '@sidequest/sqlite-backend'
* }
* });
* ```
*/
class SidequestDashboard {
/**
* The Express application instance used by the dashboard server.
* This property is optional and may be undefined if the server has not been initialized.
*/
app;
/**
* Optional dashboard configuration object that defines the behavior and appearance
* of the dashboard component. If not provided, default configuration will be used.
*/
config;
/**
* The HTTP server instance used by the dashboard application.
* This server handles incoming requests and serves the dashboard interface.
* Will be undefined until the server is started.
*/
server;
/**
* Backend instance used for server communication and data operations.
* When undefined, indicates that no backend connection has been established.
*/
backend;
constructor() {
this.app = express();
}
/**
* Starts the dashboard server with the provided configuration.
*
* @param config - Optional dashboard configuration object. If not provided, default values will be used.
* @returns A promise that resolves when the server setup is complete.
*
* @remarks
* - If the server is already running, a warning is logged and the method returns early
* - If the dashboard is disabled in config, a debug message is logged and the method returns early
* - The method sets up the Express app, backend driver, middlewares, authentication, EJS templating, routes, and starts listening
*
* @example
* ```typescript
* await dashboard.start({
* enabled: true,
* port: 3000,
* backendConfig: { driver: "@sidequest/sqlite-backend" }
* });
* ```
*/
async start(config) {
if (this.server?.listening) {
core.logger("Dashboard").warn("Dashboard is already running. Please stop it before starting again.");
return;
}
this.config = {
enabled: true,
backendConfig: {
driver: "@sidequest/sqlite-backend",
},
port: 8678,
...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) {
core.logger("Dashboard").debug(`Dashboard is disabled`);
return;
}
this.app ??= express();
this.backend = await backend.createBackendFromDriver(this.config.backendConfig);
this.setupMiddlewares();
this.setupAuth();
this.setupEJS();
this.setupRoutes();
this.listen();
}
/**
* Sets up middleware for the Express application.
*
* Configures HTTP request logging using Morgan middleware when debug logging is enabled.
* The middleware uses the "combined" format for comprehensive request logging.
*
* @remarks
* - Only adds Morgan logging middleware when debug mode is active
* - Uses Apache combined log format for detailed request information
* - Logs are handled through the Dashboard logger instance
*/
setupMiddlewares() {
core.logger("Dashboard").debug(`Setting up Middlewares`);
if (core.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();
});
}
/**
* Sets up basic authentication for the dashboard application.
*
* If authentication configuration is provided, this method configures
* HTTP Basic Authentication middleware using the specified username and password.
* The middleware will challenge unauthorized requests with a 401 response.
*
* @remarks
* - Only sets up authentication if `this.config.auth` is defined
* - Uses a single user/password combination from the configuration
* - Enables challenge mode to prompt for credentials in browsers
*
* @example
* ```typescript
* // Assuming config.auth = { user: "admin", password: "secret" }
* dashboard.setupAuth(); // Sets up basic auth for user "admin"
* ```
*/
setupAuth() {
if (this.config.auth) {
const auth = this.config.auth;
core.logger("Dashboard").debug(`Basic auth setup with User: ${auth.user}`);
const users = {};
users[auth.user] = auth.password;
this.app.use(basicAuth({
users: users,
challenge: true,
}));
}
}
/**
* Sets up EJS templating engine for the dashboard application.
* This method configures the Express application to use EJS as the view engine,
* sets the views directory, and specifies the layout file.
*
* @remarks
* - Uses `express-ejs-layouts` for layout support
* - Sets the views directory to the `views` folder within the package
* - Serves static files from the `public` directory
* - Ensures that the EJS engine is ready to render views with layouts
*/
setupEJS() {
core.logger("Dashboard").debug(`Setting up EJS`);
this.app.use(expressLayouts);
this.app.set("view engine", "ejs");
this.app.set("views", path.join(__dirname, "views"));
this.app.set("layout", path.join(__dirname, "views", "layout"));
const publicPath = this.config.basePath ? `${this.config.basePath}/public` : "/public";
this.app.use(publicPath, express.static(path.join(__dirname, "public")));
}
/**
* Sets up the main application routes for the dashboard.
*
* This method initializes and attaches the dashboard, jobs, and queues routers
* to the Express application instance. It also logs the setup process for debugging purposes.
*
* @remarks
* - Assumes that `this.app` and `this.backend` are initialized.
* - Uses the routers created by `createDashboardRouter`, `createJobsRouter`, and `createQueuesRouter`.
*/
setupRoutes() {
core.logger("Dashboard").debug(`Setting up routes`);
const basePath = this.config.basePath ?? "";
const [dashboardPath, dashboardRouter] = dashboard.createDashboardRouter(this.backend);
const [jobsPath, jobsRouter] = jobs.createJobsRouter(this.backend);
const [queuesPath, queuesRouter] = queues.createQueuesRouter(this.backend);
this.app.use(basePath + dashboardPath, dashboardRouter);
this.app.use(basePath + jobsPath, jobsRouter);
this.app.use(basePath + queuesPath, queuesRouter);
}
/**
* Starts the dashboard server on the configured port.
* Logs the startup process and handles any errors that occur during server initialization.
*
* @remarks
* If no port is specified in the configuration, the default port 8678 is used.
*
* @returns void
*/
listen() {
const port = this.config.port ?? 8678;
core.logger("Dashboard").debug(`Starting Dashboard with port ${port}`);
this.server = this.app.listen(port, (error) => {
if (error) {
core.logger("Dashboard").error("Failed to start Sidequest Dashboard!", error);
}
else {
core.logger("Dashboard").info(`Server running on http://localhost:${port}`);
}
});
}
/**
* Closes the dashboard by shutting down the backend and server,
* and cleaning up associated resources.
*
* - Awaits the closure of the backend if it exists.
* - Closes the server and logs a message when stopped.
* - Resets backend, server, config, and app properties to `undefined`.
* - Logs a debug message indicating resources have been cleaned up.
*
* @returns {Promise<void>} Resolves when all resources have been closed and cleaned up.
*/
async close() {
try {
await this.backend?.close();
}
catch (error) {
core.logger("Dashboard").error("Failed to close backend", error);
}
if (this.server) {
await new Promise((resolve) => {
this.server.close(() => {
core.logger("Dashboard").info("Sidequest Dashboard stopped");
resolve();
});
});
}
this.backend = undefined;
this.server = undefined;
this.config = undefined;
this.app = undefined;
core.logger("Dashboard").debug("Dashboard resources cleaned up");
}
}
exports.SidequestDashboard = SidequestDashboard;
//# sourceMappingURL=index.cjs.map