Notifications

This commit is contained in:
Andrii Dokhniak 2025-04-09 22:23:52 +02:00
parent 24890e9a4c
commit ca973c4114
7 changed files with 137 additions and 13 deletions

View File

@ -1,12 +1,20 @@
import { WebSocketServer } from "ws";
import child_process from "child_process";
import fs from "fs";
import { send_notification } from "./notifications.mjs"
async function spawnPromise(program, args) {
return new Promise((resolve, reject) => {
let output = "";
const process = child_process.spawn(program, args);
process.on("close", (_) => {
resolve();
process.stdout.on('data', (data) => {
output += data;
});
process.stderr.on('data', (data) => {
output += data;
});
process.on("close", (code) => {
resolve({output, code});
});
});
}
@ -32,7 +40,8 @@ wss.on("connection", (ws) => {
} else if (data === "home") {
await spawnPromise("bash", ["/conf/home.sh"]);
} else if (data === "install") {
await spawnPromise("bash", ["/conf/install.sh"]);
const res = await spawnPromise("bash", ["/conf/install.sh"]);
send_notification(res.code === 0, "Installing the application", res.output);
} else if (data.includes("drag")) {
const dataSplit = data.split(" ");

View File

@ -0,0 +1,36 @@
import { WebSocket } from "ws";
import { WebSocketServer } from "ws";
const notification_proxy = new WebSocketServer({ port: 3001 });
let notification_subs = [];
notification_proxy.on('connection', (ws) => {
notification_subs.push(ws);
});
export function send_notification(is_ok, context, message)
{
let updated_subs = [];
if (notification_subs.length === 0) {
console.log("WARNING: Got a notification, but nobody is subscribed");
}
for (const sub of notification_subs) {
if (sub.readyState == WebSocket.CONNECTING) {
console.log("WARNING: Unable to forward a notification to client that is still connecting");
updated_subs.push(sub);
} else {
try {
sub.send(JSON.stringify({
is_ok,
context,
message
}));
updated_subs.push(sub);
} catch {
sub.close();
console.log("WARNING: Fail to send a notification, closing the connection");
}
}
}
}

View File

@ -22,6 +22,7 @@ services:
- 45457:45459 # This cannot change
- 10001:10001 # api port
- 3000:3000 # android server port
- 3001:3001 # Notifications server
volumes:
- $PWD/shared_buffer:/shared_buffer
- $PWD/android/conf:/conf

View File

@ -128,10 +128,16 @@
#upload_form button:hover, #upload_form label:hover {
background-color: #ddd;
}
#notifications {
width: 40%;
margin-left: 60%;
position: absolute;
}
</style>
</head>
<body>
<div id="resp"></div>
<div id="notifications"></div>
<div id="resp" style="display: none;"></div>
<main>
<section class="screen-section" >
<img
@ -316,5 +322,6 @@
screenshot_loop();
</script>
<script src="/trafficLog.js"></script>
<script src="/notifications.js"></script>
</body>
</html>

View File

@ -16,6 +16,8 @@ const device_size_y = 640;
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.static('/code/dist'))
console.log("Waiting for full boot...");
await waitFullBoot();
console.log("Boot detected! activating endpoints");
@ -25,10 +27,6 @@ app.get("/favicon.ico", function (req, res) {
res.sendFile("/code/favicon.ico");
});
app.get("/trafficLog.js", function (req, res) {
res.sendFile("/code/dist/trafficLog.js");
});
app.get("/htmx.js", function (req, res) {
res.sendFile("/code/node_modules/htmx.org/dist/htmx.min.js");
});
@ -69,11 +67,15 @@ app.post('/upload_apk', async function (req, res) {
return res.status(400).send('No files were uploaded.');
}
execSync("rm -rf /shared_buffer/*");
console.log(req.files);
if (Array.isArray(req.files.app)) {
for (const [idx, file] of req.files.app.entries()) {
let uploadPath = '/shared_buffer/app' + idx + '.apk';
await file.mv(uploadPath);
}
} else {
let uploadPath = '/shared_buffer/app' + 0 + '.apk';
await req.files.app.mv(uploadPath);
}
android_websocket.send(`install`);
res.send('Files uploaded!');
})

View File

@ -1,6 +1,6 @@
{
"scripts": {
"build": "esbuild --sourcemap --bundle src/trafficLog.jsx --outfile=dist/trafficLog.js --jsx-factory=h --jsx-fragment=Fragment"
"build": "esbuild --sourcemap --bundle src/trafficLog.jsx src/notifications.jsx --outdir=dist/ --jsx-factory=h --jsx-fragment=Fragment"
},
"dependencies": {
"express": "^4.18.2",

View File

@ -0,0 +1,69 @@
import { render, Component } from "preact";
import { useState } from "preact/hooks";
function rand_num() {
return Math.floor(Math.random() * Number.MAX_VALUE);
}
class Notifications extends Component {
constructor() {
super();
this.state = { notifications: [] };
}
remove_notification (id) {
const newNotifications = this.state.notifications.filter(
(notification) => notification.id !== id
);
this.setState({ notifications: newNotifications });
};
componentDidMount() {
// This should also be dynamic
this.connection = new WebSocket("ws://127.0.0.1:3001");
this.connection.onmessage = (msg) => {
let new_id = rand_num();
this.setState({
notifications: [
{ id: new_id, notification: JSON.parse(msg.data) },
...this.state.notifications,
],
});
// a 10 sec timeout
setTimeout(() => {
this.remove_notification(new_id)
}, 10000)
};
}
render() {
console.log("Render", this.state.notifications);
return this.state.notifications.map(({ id, notification }) => (
<div
onClick={() => this.remove_notification(id)}
style={`
background-color: ${notification.is_ok ? "#66ff99" : "#ff5c33"};
border-radius: 5px;
border-width: 2px;
border-style: solid;
border-color: ${notification.is_ok ? "#369648" : "#a23915"};
padding: 5px;
margin-top: 2px;
margin-bottom: 2px;
`}
>
<div>
<b>{notification.context}</b>
</div>
<div>
{notification.message.split("\n").map((line) => (
<p>{line}</p>
))}
</div>
</div>
));
}
}
render(<Notifications />, document.getElementById("notifications"));