This commit is contained in:
sfja 2025-07-01 23:06:08 +02:00
commit 2f7c19f28e
12 changed files with 420 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
data/*
mlread

1
data/.gitkeep Normal file
View File

@ -0,0 +1 @@

9
deno.jsonc Normal file
View File

@ -0,0 +1,9 @@
{
"fmt": {
"indentWidth": 4
},
"tasks": {
"run": "deno run --allow-net --allow-env=PORT --allow-read=./public,./data --allow-write=./public,./data main.ts",
"compile": "deno compile --allow-net --allow-env=PORT --allow-read=./public,./data --allow-write=./public,./data --output mlread main.ts"
}
}

80
deno.lock generated Normal file
View File

@ -0,0 +1,80 @@
{
"version": "4",
"specifiers": {
"jsr:@oak/commons@1": "1.0.0",
"jsr:@oak/oak@*": "17.1.3",
"jsr:@std/assert@1": "1.0.8",
"jsr:@std/bytes@1": "1.0.4",
"jsr:@std/bytes@^1.0.2": "1.0.4",
"jsr:@std/crypto@1": "1.0.3",
"jsr:@std/encoding@1": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/http@1": "1.0.10",
"jsr:@std/io@0.224": "0.224.9",
"jsr:@std/media-types@1": "1.1.0",
"jsr:@std/path@1": "1.0.8",
"npm:path-to-regexp@6.2.1": "6.2.1"
},
"jsr": {
"@oak/commons@1.0.0": {
"integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac",
"dependencies": [
"jsr:@std/assert",
"jsr:@std/bytes@1",
"jsr:@std/crypto",
"jsr:@std/encoding@1",
"jsr:@std/http",
"jsr:@std/media-types"
]
},
"@oak/oak@17.1.3": {
"integrity": "d89296c22db91681dd3a2a1e1fd14e258d0d5a9654de55637aee5b661c159f33",
"dependencies": [
"jsr:@oak/commons",
"jsr:@std/assert",
"jsr:@std/bytes@1",
"jsr:@std/crypto",
"jsr:@std/http",
"jsr:@std/io",
"jsr:@std/media-types",
"jsr:@std/path",
"npm:path-to-regexp"
]
},
"@std/assert@1.0.8": {
"integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b"
},
"@std/bytes@1.0.4": {
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
},
"@std/crypto@1.0.3": {
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
},
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
},
"@std/http@1.0.10": {
"integrity": "4e32d11493ab04e3ef09f104f0cb9beb4228b1d4b47c5469573c2c294c0d3692",
"dependencies": [
"jsr:@std/encoding@^1.0.5"
]
},
"@std/io@0.224.9": {
"integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3",
"dependencies": [
"jsr:@std/bytes@^1.0.2"
]
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
}
},
"npm": {
"path-to-regexp@6.2.1": {
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
}
}
}

50
main.ts Normal file
View File

@ -0,0 +1,50 @@
import * as oak from "jsr:@oak/oak";
const port = Number(Deno.env.get("PORT") ?? 8000);
const router = new oak.Router();
router.post("/api/log", async (ctx) => {
const data = await ctx.request.body.json();
const enc = new TextEncoder();
const postLog = await Deno.open("./data/log.txt", {
append: true,
create: true,
});
postLog.write(
enc.encode(`${new Date().toISOString()}\t${JSON.stringify(data)}\n`),
);
ctx.response.body = { ok: true };
ctx.respond = true;
});
router.get("/api/log", async (ctx) => {
const postLog = await Deno.readTextFile("./data/log.txt");
const entries = postLog
.split("\n")
.filter((l) => l)
.map((entry) => entry.split("\t"))
.map(([timestamp, data]) => (console.log(timestamp, data), {
timestamp: new Date(timestamp),
data: JSON.parse(data),
}));
ctx.response.body = entries;
ctx.respond = true;
});
console.log(`listening at http://localhost:${port}/`);
new oak.Application()
.use(router.routes())
.use(router.allowedMethods())
.use(async (ctx) => {
const { request: { url: { pathname } } } = ctx;
await oak.send(ctx, pathname, {
root: "./public",
index: "index.html",
});
})
.listen({ port });

14
mlread.service Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=mlread webservice
[Service]
Type=simple
User=simone
ExecStart=/home/simone/mlread/mlread
WorkingDirectory=/home/simone/mlread/
Restart=on-failure
Environment="PORT=8300"
[Install]
WantedBy=default.target

9
public/deno.jsonc Normal file
View File

@ -0,0 +1,9 @@
{
"fmt": {
"indentWidth": 4
},
"compilerOptions": {
"lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"]
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

56
public/index.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>philopsher's ml reading</title>
<link rel="stylesheet" href="style.css">
<script src="index.js" type="module" defer></script>
</head>
<body>
<main>
<h1>philopsher's ml reading group</h1>
<p>The plan is to read and discuss texts from the <b>ML test</b> materials in the <b>#test-materials</b> channels in FinBol's. (<a href="https://discord.com/channels/384015097284001792/874424650057257022" target="_blank">Link</a>)</p>
<h2>The Three Sources and Three Component Parts of Marxism by Vladimir Lenin</h2>
<p><b>To be scheduled...</b></p>
<p>This text is available both <a href="https://www.marxists.org/archive/lenin/works/1913/mar/x01.htm" target="_blank">as text at MIA</a> and <a href="https://www.youtube.com/watch?v=MOAII71GaFY" target="_blank">as audiobook by Socialism For All</a>.</p>
<p>The work can be read in 20 minutes. The plan is to start the event 20 minutes <b>before</b> the announced time, to give people time to read the work, if they haven't already. Then <b>at</b> the announced time, the presentation/discussion will start. I will go through what I think are the main points, and we will use this presentation as foundation for discussion.</p>
<p>I may put some notes here afterwards...</p>
<h1>Scheduler</h1>
<div id="scheduler">
<p>
Select as many options as possible. I will try and find the
best timeslot.
</p>
<p>
Timestamps are UTC, which is currently <b><span id="time-utc">00:00</span></b>. Compensate according to your own timezone.
</p>
<form action="#" id="schedule-form">
<div id="schedule-table-scroller">
<table id="schedule-table">
<tr><th>loading...</th></tr>
</table>
</div>
<div id="tail">
<div>
<label for="input-username">Discord username:</label>
<input type="text" name="username" id="input-username">
</div>
<div>
<label for="input-note">Note (optional):</label>
<textarea name="note" id="input-note" rows="2" cols="30"></textarea>
</div>
<div>
<label for="tos">I will jack your shit.</label>
<input type="checkbox" name="tos" id="tos">
</div>
</div>
<div id="submit-div">
<input type="submit" value="Submit">
</div>
</form>
</div>
</main>
<script></script>
</body>
</html>

108
public/index.js Normal file
View File

@ -0,0 +1,108 @@
const timeUtcSpan = document.querySelector("#time-utc");
const scheduleTable = document.querySelector("#schedule-table");
const scheduleForm = document.querySelector("#schedule-form");
timeUtcSpan.innerText = new Date()
.toUTCString()
.slice(17, 22);
// .toLocaleTimeString(["en-UK"], { hour: "2-digit", minute: "2-digit" });
const weekDayNames = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
async function makeScheduleTable() {
const timeslotData = await fetch("timeslots.json")
.then((res) => res.json());
const timeslots = timeslotData
.map(({ date, times }) => ({
dateString: date,
date: new Date(date),
times,
}))
.toSorted((a, b) => a.date.getTime() - b.date.getTime());
let tableHtml = `<tr>
${
timeslots.map((ts) =>
`<th>${weekDayNames[ts.date.getUTCDay()]} ${ts.date.getUTCDay()}. ${
monthNames[ts.date.getUTCMonth()]
}</th>`
)
.join("")
}
</tr>`;
tableHtml += "<tr>";
for (const timeslot of timeslots) {
tableHtml += "<td>";
for (const time of timeslot.times) {
const name = `${timeslot.dateString}-${time}`;
tableHtml += `
<div>
<input type="checkbox" name="${name}" id="checkbox-${name}">
<label for="checkbox-${name}">${time}</label>
</div>
`;
}
tableHtml += "</td>";
}
tableHtml += "</tr>";
scheduleTable.innerHTML = tableHtml;
}
scheduleForm.onsubmit = async (ev) => {
ev.preventDefault();
const formdata = new FormData(ev.target);
const data = formdata.entries().reduce(
(acc, [key, val]) => ({ ...acc, [key]: val }),
{},
);
if (!data.username) {
alert("please specify username");
return;
}
if (!data.tos) {
alert("please check information agreement");
return;
}
console.log(data);
const response = await fetch("/api/log", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res.json());
if (response.ok) {
alert("submitted!");
} else {
alert("error occured!");
console.log(response);
}
};
makeScheduleTable();

69
public/style.css Normal file
View File

@ -0,0 +1,69 @@
*, *::before, *::after {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
}
body {
margin: 0 auto;
padding: 2rem;
max-width: 1000px;
line-height: 1.6em;
}
li {
margin-top: 1ex;
}
table {
border-collapse: collapse;
}
table td, table th {
border: 1px solid;
padding: 4px;
}
#scheduler {
border: 1px solid;
padding: 10px;
}
#scheduler form {
display: flex;
flex-direction: column;
justify-content: start;
align-items: center;
gap: 10px;
}
#scheduler form #tail {
width: 50%;
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;
gap: 10px;
}
#scheduler #submit-div {
width: 100%;
text-align: center;
}
#scheduler form input[type="submit"] {
width: 30%;
padding: 5px;
}
#schedule-table-scroller {
width: 100%;
overflow: scroll;
padding-bottom: 20px;
}
#schedule-table {
width: max-content;
}

21
public/timeslots.json Normal file
View File

@ -0,0 +1,21 @@
[
{ "date": "7-1-2025", "times": ["18:00", "21:00"] },
{ "date": "7-2-2025", "times": ["18:00", "21:00"] },
{ "date": "7-3-2025", "times": ["18:00", "21:00"] },
{ "date": "7-4-2025", "times": ["18:00", "21:00"] },
{ "date": "7-5-2025", "times": [] },
{ "date": "7-6-2025", "times": ["13:00", "15:00", "18:00", "21:00"] },
{ "date": "7-7-2025", "times": ["18:00", "21:00"] },
{ "date": "7-8-2025", "times": ["18:00", "21:00"] },
{ "date": "7-9-2025", "times": ["18:00", "21:00"] },
{ "date": "7-10-2025", "times": ["18:00", "21:00"] },
{ "date": "7-11-2025", "times": [] },
{
"date": "7-12-2025",
"times": ["13:00", "15:00", "18:00", "21:00", "23:00"]
},
{ "date": "7-13-2025", "times": ["13:00", "15:00", "18:00", "21:00"] },
{ "date": "7-14-2025", "times": ["18:00", "21:00"] },
{ "date": "7-15-2025", "times": ["18:00", "21:00"] },
{ "date": "7-16-2025", "times": ["18:00", "21:00"] }
]