diff --git a/.gitignore b/.gitignore index d8b3854..869fb51 100644 --- a/.gitignore +++ b/.gitignore @@ -104,7 +104,6 @@ dist .tern-port # Custom -*.js assets/css/ assets/js/ assets/webp/ diff --git a/package-lock.json b/package-lock.json index e4fc28c..c9e3ddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "body-parser": "^1.20.3", "ejs": "3.1.10", "express": "^4.21.2", + "express-ws": "^5.0.2", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5" @@ -19,6 +20,7 @@ "devDependencies": { "@types/csso": "5.0.4", "@types/express": "^5.0.0", + "@types/express-ws": "3.0.5", "@types/node": "^22.10.5", "@types/uglify-js": "3.17.5", "csso": "5.0.5", @@ -412,6 +414,18 @@ "@types/send": "*" } }, + "node_modules/@types/express-ws": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/express-ws/-/express-ws-3.0.5.tgz", + "integrity": "sha512-lbWMjoHrm/v85j81UCmb/GNZFO3genxRYBW1Ob7rjRI+zxUBR+4tcFuOpKKsYQ1LYTYiy3356epLeYi/5zxUwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/ws": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -494,6 +508,16 @@ "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -930,6 +954,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "license": "BSD-2-Clause", + "dependencies": { + "ws": "^7.4.6" + }, + "engines": { + "node": ">=4.5.0" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0-alpha.1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -2102,6 +2141,27 @@ "@types/node": "*" } }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2b82fbf..72f76ae 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "body-parser": "^1.20.3", "ejs": "3.1.10", "express": "^4.21.2", + "express-ws": "^5.0.2", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5" @@ -30,6 +31,7 @@ "devDependencies": { "@types/csso": "5.0.4", "@types/express": "^5.0.0", + "@types/express-ws": "3.0.5", "@types/node": "^22.10.5", "@types/uglify-js": "3.17.5", "csso": "5.0.5", diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..6540c5c --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,61 @@ +document.addEventListener('DOMContentLoaded', function () { + const logContainer = this.getElementById('logs'); + const followLogsBtn = this.getElementById('followCheckmark'); + const buildStatusTxt = this.getElementById('buildStatus'); + + /** + * Get the correct path to establish a ws connection + * @param {Location} loc + */ + function wsPath(loc) { + return loc.pathname.replace(/\/$/, '') + '/ws'; + } + + /** + * Add log line to the DOM + * @param {string} str + */ + function appendLine(str, e = false) { + const p = document.createElement('p'); + p.appendChild(document.createTextNode(str)); + logContainer.appendChild(p); + } + + /** + * Scroll to bottom of page if checkbox is checked + */ + function scrollToBottom() { + if (followLogsBtn.checked) { + window.scrollTo(0, document.body.scrollHeight); + } + } + + /** + * Establish websocket connection + */ + function connect() { + const loc = window.location; + let new_uri = loc.protocol === 'https:' ? 'wss:' : 'ws:'; + new_uri += "//" + loc.host; + new_uri += wsPath(loc); + var ws = new WebSocket(new_uri); + + ws.onmessage = function (message) { + console.log('Got message: ', message); + const buildEvent = JSON.parse(message.data); + + if (buildEvent.type === 'finish') { + ws.close(); + buildStatusTxt.replaceChild(document.createTextNode(buildEvent.message), buildStatusTxt.firstChild); + } + else { + appendLine(buildEvent.message, buildEvent.type === 'err'); + scrollToBottom(); + } + } + } + + connect(); + followLogsBtn.checked = false; + followLogsBtn.addEventListener('change', scrollToBottom); +}); \ No newline at end of file diff --git a/src/BuildController.ts b/src/BuildController.ts index 48adb78..a43f2f2 100644 --- a/src/BuildController.ts +++ b/src/BuildController.ts @@ -19,10 +19,11 @@ class BuildController extends EventEmitter { private db: DB; private process: spawn.ChildProcess | null = null; private running: boolean = false; + private interval: NodeJS.Timeout; constructor(config = {}) { super(); - setInterval(this.triggerBuild, 60000); + // this.interval = setInterval(this.triggerBuild, 60000); } triggerBuild = () => { @@ -106,18 +107,18 @@ class BuildController extends EventEmitter { }); docker.on('close', (code) => { this.process = null; + const status = code === 0 ? 'success' : 'error'; this.emitLog({ id: build.id, type: 'finish', - message: code + message: status }); + this.db.finishBuild(build.id, status); if (code === 0) { - this.db.finishBuild(build.id, 'success'); resolve(); } else { - this.db.finishBuild(build.id, 'error'); reject(code); } }); @@ -149,6 +150,12 @@ class BuildController extends EventEmitter { setDB = (db: DB) => { this.db = db; } + + close = () => { + if (this.interval) { + clearInterval(this.interval); + } + } } export default BuildController; diff --git a/src/Web.ts b/src/Web.ts index ad94c8c..4676ea9 100644 --- a/src/Web.ts +++ b/src/Web.ts @@ -1,10 +1,11 @@ import * as http from "http"; import crypto from 'crypto'; import type { Express } from "express"; -import express, { application } from 'express'; +import express from 'express'; +import expressWs from "express-ws"; import bodyParser from "body-parser"; import type { DB } from "./DB.ts"; -import type { BuildController } from "./BuildController.ts"; +import type { BuildController, BuildEvent } from "./BuildController.ts"; interface WebConfig { port?: number; @@ -26,6 +27,7 @@ class Web { constructor(options: WebConfig = {}) { const app: Express = this.app = express(); + const wsApp = expressWs(app).app; this.port = notStupidParseInt(process.env.PORT) || options['port'] as number || 8080; app.set('trust proxy', 1); @@ -95,7 +97,8 @@ class Web { description: `Building ${build.repo} on ${build.distro}` }, build, - log + log, + ended: build.status !== 'queued' && build.status !== 'running' }); }); @@ -112,6 +115,22 @@ class Web { app.get('/healthcheck', (_, res) => { res.send('Healthy'); }); + + wsApp.ws('/build/:num/ws', (ws, req) => { + console.log('WS Opened'); + const eventListener = (be: BuildEvent) => { + if (be.id === notStupidParseInt(req.params.num)) { + ws.send(JSON.stringify(be)); + } + }; + this.buildController.on('log', eventListener); + + ws.on('close', () => { + console.log('WS Closed'); + this.buildController.removeListener('log', eventListener); + }); + }); + } close = () => { diff --git a/src/index.ts b/src/index.ts index 629b35c..ab98ded 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,4 +24,5 @@ buildController.setDB(db); process.on('SIGTERM', () => { web.close(); db.close(); + buildController.close(); }); diff --git a/styles/01-styles.scss b/styles/01-styles.scss index c854a80..9c7ad0b 100644 --- a/styles/01-styles.scss +++ b/styles/01-styles.scss @@ -105,11 +105,13 @@ input[type="submit"] { display: inline-block; padding: .3em .6em; color: #fff; + margin-bottom: .4em; box-shadow: 0px .4em 0 0 var(--primary-dark); &:active { margin-top: .4em; + margin-bottom: 0; box-shadow: 0px .1em 0 0 var(--primary-dark); } } @@ -238,3 +240,17 @@ input[type="submit"] { } } } + +#followCheckmarkContainer { + display: inline-block; + position: fixed; + right: 1.3em; + bottom: 1.6em; + padding: 1em; + background-color: rgba(15,15,15,.8); + cursor: pointer; + + input { + cursor: inherit; + } +} diff --git a/views/build.ejs b/views/build.ejs index 8b16948..9ce7364 100644 --- a/views/build.ejs +++ b/views/build.ejs @@ -9,12 +9,13 @@ <%- include("navigation", locals) %>

Build #<%= build.id %>

-

<%= build.status %>

+

<%= build.status %>

<%= build.repo %> <% if (build.commit) { %><%= build.commit %><% } else { %>latest<% } %> <% if (build.patch) { %>patch file<% } else { %>none<% } %> <%= build.distro %> + <%= build.dependencies %> <%= build.startTime %>

Full logs

@@ -23,7 +24,16 @@

<%= line %>

<% }) %>
+ + <% if (!ended) { %> + + <% } %> <%- include("footer", locals) %> + <% if (!ended) { %> + + <% } %> diff --git a/views/navigation.ejs b/views/navigation.ejs index cb7f340..edf7c46 100644 --- a/views/navigation.ejs +++ b/views/navigation.ejs @@ -9,11 +9,11 @@ \ No newline at end of file