7 Commits

Author SHA1 Message Date
28f18621e0 the LUG update
All checks were successful
NPM Audit Check / Check NPM audit (push) Successful in -2m5s
App Image CI / Build app image (push) Successful in 1m9s
2025-12-05 12:00:03 -05:00
5e1c783a23 bump deps
All checks were successful
App Image CI / Build app image (push) Successful in 1m8s
NPM Audit Check / Check NPM audit (push) Successful in -2m2s
2025-12-01 19:28:08 -05:00
a934b13df6 post event
Some checks failed
App Image CI / Build app image (push) Successful in 1m3s
NPM Audit Check / Check NPM audit (push) Failing after -2m4s
2025-11-01 20:39:11 -05:00
c86afff89c firefox script
All checks were successful
NPM Audit Check / Check NPM audit (push) Successful in -2m8s
App Image CI / Build app image (push) Successful in 1m1s
2025-11-01 13:58:40 -05:00
7582d1054e 0.0.6
All checks were successful
NPM Audit Check / Check NPM audit (push) Successful in -2m6s
App Image CI / Build app image (push) Successful in 1m4s
2025-11-01 00:37:14 -05:00
77ea3018c1 create admin form
All checks were successful
App Image CI / Build app image (push) Successful in 37s
NPM Audit Check / Check NPM audit (push) Successful in -2m8s
2025-11-01 00:31:52 -05:00
f5c9d8c0c4 Add install count to homepage 2025-10-31 23:18:18 -05:00
13 changed files with 288 additions and 85 deletions

View File

@@ -7,5 +7,10 @@ services:
context: ./
dockerfile: Dockerfile
restart: "no"
environment:
ADMINPARAM: password
ADMINPASS: letmein
volumes:
- ./config/:/usr/src/app/config/
ports:
- 8080:8080

111
package-lock.json generated
View File

@@ -1,23 +1,24 @@
{
"name": "madisonlinux",
"version": "0.0.5",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "madisonlinux",
"version": "0.0.5",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"body-parser": "^2.2.1",
"ejs": "3.1.10",
"express": "5.1.0",
"express": "^5.2.1",
"ics": "^3.8.1",
"json5": "2.2.3"
},
"devDependencies": {
"@sindresorhus/tsconfig": "8.0.1",
"@types/express": "^5.0.3",
"@types/node": "^24.7.0",
"@types/express": "^5.0.6",
"@types/node": "^24.10.1",
"forking-build-shit": "1.0.5",
"typescript": "5.9.3"
}
@@ -367,15 +368,15 @@
}
},
"node_modules/@types/express": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "*"
"@types/serve-static": "^2"
}
},
"node_modules/@types/express-serve-static-core": {
@@ -398,21 +399,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz",
"integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==",
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
"undici-types": "~7.16.0"
}
},
"node_modules/@types/qs": {
@@ -440,25 +434,13 @@
}
},
"node_modules/@types/serve-static": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
"integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "<1"
}
},
"node_modules/@types/serve-static/node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
@@ -488,23 +470,27 @@
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.0",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.6.3",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": {
@@ -782,18 +768,19 @@
}
},
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
@@ -1005,15 +992,19 @@
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ics": {
@@ -1376,22 +1367,6 @@
"node": ">= 0.10"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -1698,9 +1673,9 @@
}
},
"node_modules/undici-types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,6 +1,6 @@
{
"name": "madisonlinux",
"version": "0.0.5",
"version": "0.2.0",
"description": "Website for upcoming Linux install party in Madison WI",
"keywords": [
"web",
@@ -24,15 +24,16 @@
"views"
],
"dependencies": {
"body-parser": "^2.2.1",
"ejs": "3.1.10",
"express": "5.1.0",
"express": "^5.2.1",
"ics": "^3.8.1",
"json5": "2.2.3"
},
"devDependencies": {
"@sindresorhus/tsconfig": "8.0.1",
"@types/express": "^5.0.3",
"@types/node": "^24.7.0",
"@types/express": "^5.0.6",
"@types/node": "^24.10.1",
"forking-build-shit": "1.0.5",
"typescript": "5.9.3"
},

28
scripts/form.js Normal file
View File

@@ -0,0 +1,28 @@
document.addEventListener('DOMContentLoaded', function () {
const submit = document.getElementById('submitbtn');
submit.addEventListener('click', async () => {
submit.disabled = true;
const action = submit.parentElement.action;
try {
const resp = await fetch(action, {
method: 'POST',
body: JSON.stringify({
os: document.getElementById('os-select').value,
form: document.getElementById('form-select').value
}),
headers: {
"Content-Type": "application/json",
}
});
if (!resp.ok) {
throw new Error(`Response status: ${resp.status}`);
}
location.reload();
return false;
}
catch (error) {
console.error(error.message);
alert(error.message);
}
});
});

View File

@@ -4,11 +4,18 @@ import express from 'express';
import bodyParser from 'body-parser';
import type { Express } from 'express';
import { createEvent } from 'ics';
import path from 'path';
import { promises as fsp } from 'fs';
interface WebConfig {
port?: number;
}
interface Install {
os: 'manjaro' | 'mint' | 'chrome' | 'other',
form: 'laptop' | 'desktop' | 'aio'
}
const DATE = process.env['DATE'] || 'November 1st';
const TIME = process.env['TIME'] || '2:30PM-5:30PM';
@@ -23,6 +30,7 @@ class Web {
private _webserver: http.Server | null = null;
private app: Express | null = null;
private port: number;
private installs: Install[] = [];
// private options: WebConfig;
constructor(options: WebConfig = {}) {
@@ -33,13 +41,21 @@ class Web {
initialize = async () => {
// const options = this.options;
const app: Express = this.app = express();
const installPath = process.env['installstat'] || process.env['INSTALLSTAT'] || path.join(process.cwd(), 'config', 'installs.json');
try {
this.installs = JSON.parse(await fsp.readFile(installPath, 'utf-8'));
}
catch (ex) {
console.error(ex);
this.installs = [];
}
app.set('trust proxy', 1);
app.set('view engine', 'ejs');
app.set('view options', { outputFunctionName: 'echo' });
app.use('/assets', express.static('assets', { maxAge: '30 days' }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((_req, res, next) => {
crypto.randomBytes(32, (err, randomBytes) => {
if (err) {
@@ -56,24 +72,47 @@ class Web {
res.send('Healthy');
});
const adminParam = process.env['ADMINPARAM'];
const adminPass = process.env['ADMINPASS'];
app.get('/', (req, res) => {
const adminMode = adminParam && adminPass && adminParam in req.query && req.query[adminParam] === adminPass;
if (req.query?.['utm_medium']) {
console.log(`${req.query['utm_medium']} | ${req.headers?.['user-agent']}`);
}
else {
else if(!adminMode){
console.log(req.headers?.['user-agent']);
}
res.render('index', {
res.render('lug', {
page: {
title: 'Madison End of 10 Install Party',
titlesuffix: 'Get help installing Linux',
description: `Windows 10 support is ending, but you may not need a brand-new PC! Sector67 in Madison, Wisconsin is hosting a Linux install party to help the community keep their current computers usable and up-to-date. Join us on ${DATE}`
},
date: DATE,
time: TIME
installs: this.installs,
time: TIME,
adminMode
});
});
app.post('/', async (req, res) => {
const adminMode = adminParam && adminPass && adminParam in req.query && req.query[adminParam] === adminPass;
if (!adminMode) {
res.redirect('/');
return;
}
const submit = req.body;
if (!('os' in submit && 'form' in submit)) {
res.status(400).send('bad request');
return;
}
this.installs.push(submit);
console.log(`updating ${installPath}`);
await fsp.writeFile(installPath, JSON.stringify(this.installs, null, 2));
res.send('ok');
});
app.get('/os', (_, res) => {
res.render('os', {
page: {
@@ -84,20 +123,28 @@ class Web {
});
});
app.get('/firefox', (_, res) => {
res.redirect('https://gist.github.com/CorySanin/d322dc6ebe534c23ba183920d154e566');
});
app.get('/event.ics', (_, res) => {
createEvent({
uid: '1@madisonlinux.com',
start: [2025, 11, 1, 19, 30],
duration: { hours: 3, minutes: 0 },
title: 'Madison Linux Workshop',
description: 'Keep your current PC up-to-date by installing a free OS!',
uid: '2@madisonlinux.com',
sequence: 1,
startInputType: 'local',
startOutputType: 'local',
start: [2026, 1, 15, 19, 0],
duration: { hours: 1, minutes: 0 },
title: 'Madison Linux User Group',
description: 'A place for Linux users of all skill levels to meet and share knowledge, troubleshoot, and show off',
location: '56 Corry St, Madison, WI 53704',
url: 'https://madisonlinux.com/',
geo: { lat: 43.0982199, lon: -89.3481373 },
status: 'CONFIRMED',
busyStatus: 'TENTATIVE',
organizer: { name: 'Cory Sanin', email: 'endof10@cory.sanin.dev' },
method: 'REQUEST'
method: 'REQUEST',
recurrenceRule: 'FREQ=MONTHLY;BYDAY=TH;BYSETPOS=3'
}, (err, s) => {
if (err) {
console.error(err);

View File

@@ -73,6 +73,34 @@ img {
max-width: 100%;
}
#installs {
h1 {
animation-iteration-count: infinite;
animation-name: zoomie;
animation-duration: 1.5s;
animation-timing-function: ease-out;
}
span {
padding: .05em .25em;
border-radius: .15em;
}
}
@keyframes zoomie {
0% {
transform: perspective(800px) translate3d(0, 0, 0px);
}
50% {
transform: perspective(800px) translate3d(0, 0, 40px);
}
100% {
transform: perspective(800px) translate3d(0, 0, 0px);
}
}
@media screen and (max-width:540px) {
.content {
width: 95%;

View File

@@ -44,4 +44,8 @@ a.btn {
color: #fff;
}
}
}
}
#installs span {
background: #1A237E;
}

View File

@@ -14,7 +14,7 @@
<link rel="canonical" href="<%= page.canonical%>"/>
<% } %>
<link rel="shortcut icon" href="/assets/svg/favicon.svg">
<link rel="stylesheet" href="/assets/css/styles.css?v5">
<link rel="stylesheet" href="/assets/css/styles.css?v6">
<script nonce="<%= cspNonce %>">
document.addEventListener("DOMContentLoaded", function() {
document.body.classList.remove('preload');

View File

@@ -8,6 +8,14 @@
<body class="preload">
<%- include("navigation", locals) %>
<div class="content">
<% if (adminMode) { %>
<%- include("installform", locals) %>
<% } %>
<% if (installs && installs.length) { %>
<div id="installs">
<h1>We have saved <span><%= installs.length %></span> computer<% if (installs.length > 1) { %>s<% } %>!</h1>
</div>
<% } %>
<p><a href="https://www.sector67.org/">Sector67</a> presents...</p>
<h1>Linux Install Party Madison</h1>
<h2>Are you ready to reduce e-waste and save some money?</h2>

38
views/index_after.ejs Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<%- include("head", locals) %>
</head>
<body class="preload">
<%- include("navigation", locals) %>
<div class="content">
<% if (adminMode) { %>
<%- include("installform", locals) %>
<% } %>
<h1>Linux Install Party Madison</h1>
<% if (installs && installs.length) { %>
<div id="installs">
<h1>We have saved <span><%= installs.length %></span> computer<% if (installs.length > 1) { %>s<% } %>!</h1>
</div>
<% } %>
<h2>Thanks for saving your computers!</h2>
<p>
Thank you all for attending! It was great meeting everyone and I hope you all
enjoy your new OS's! Thanks to <a href="https://www.sector67.org/">Sector67</a>
for hosting the event and recruiting volunteers as well.
</p>
<p>
No promises, but perhaps we'll do this again in the future. You better believe if
Windows 10 ends up in the news due to a major vulnerability, I will almost certainly
be contemplating organizing another event. So stay tuned.
</p>
<h2>Need help?</h2>
<p>
If we set up your Linux installation and you need help or guidance, shoot me an
email at <a href="mailto:endof10@cory.sanin.dev">endof10@cory.sanin.dev</a>.
</p>
</div>
</body>
</html>

19
views/installform.ejs Normal file
View File

@@ -0,0 +1,19 @@
<div>
<select name="OS" id="os-select">
<option value="mint">Mint</option>
<option value="manjaro">Manjaro</option>
<option value="chrome">ChromeOS</option>
<option value="other">Other</option>
</select>
</div>
<div>
<select name="form" id="form-select">
<option value="laptop">Laptop</option>
<option value="desktop">Desktop</option>
<option value="aio">AIO</option>
</select>
</div>
<form method="post">
<input type="button" value="Submit" id="submitbtn" />
</form>
<script src="/assets/js/form.js?v1" nonce="<%= cspNonce %>"></script>

51
views/lug.ejs Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<%- include("head", locals) %>
</head>
<body class="preload">
<%- include("navigation", locals) %>
<div class="content">
<% if (adminMode) { %>
<%- include("installform", locals) %>
<% } %>
<h1>Madison Linux</h1>
<p>
A Linux user Group (LUG) is coming to your neighborhood...
</p>
<p>
Madison Linux is (will be?) a place for Linux users of all skill levels to meet and share knowledge, troubleshoot, and show off.
Completely open to the pulic, we will meet monthly starting in January 2026. Every 3rd Thursday, at 7PM.
<a href="https://www.sector67.org/">Sector67</a> is hosting us again, so a big thank you to them!
</p>
<p>
<a class="btn" href="/event.ics">🗓️ Add to Calendar</a>
</p>
<p>
Agenda for January 15th:
<ul>
<li>Decide on what communication platform(s) will we use</li>
<li>Casual presentation on the format of other Linux User Groups; how-to LUG</li>
<li>Get a sense where the community's interests lie</li>
<li>Some time to meet and chat</li>
</ul>
</p>
<p>
Hope to see you soon!
</p>
<hr/>
<% if (installs && installs.length) { %>
<div id="installs">
<h1>We have saved <span><%= installs.length %></span> computer<% if (installs.length > 1) { %>s<% } %>!</h1>
</div>
<% } %>
<h2>Need help?</h2>
<p>
If we set up your Linux installation and you need help or guidance, shoot me an
email at <a href="mailto:endof10@cory.sanin.dev">endof10@cory.sanin.dev</a>.
</p>
</div>
</body>
</html>

View File

@@ -2,7 +2,6 @@
<nav>
<ul class="nav_links">
<li><a href="/">Home</a></li>
<li><a href="/#event">Event&nbsp;Details</a></li>
<li><a href="/os">OS&nbsp;Recommendations</a></li>
</ul>
</nav>