first commit
This commit is contained in:
commit
25de9638dd
30 changed files with 4541 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
36
README.md
Normal file
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Dashboard per tutte le mie applicazioni
|
||||||
|
|
||||||
|
entrare in frontend: installare e fare la build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /frontend
|
||||||
|
npm ci install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
entrare nella backend e installare
|
||||||
|
```sh
|
||||||
|
cd /backend
|
||||||
|
npm ci install
|
||||||
|
```
|
||||||
|
|
||||||
|
editare il file app.json con host e port
|
||||||
|
|
||||||
|
vedi esempio apps1.json
|
||||||
|
|
||||||
|
facendo partire il server il file diventerà come apps.json
|
||||||
|
|
||||||
|
infine far partire il server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
se si vogliono fare delle prove di creazione del file apps.json si può copiare apps1.json
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp apps1.json apps.json
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
226
backend/apps.json
Normal file
226
backend/apps.json
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3100,
|
||||||
|
"name": "Forgejo: Beyo..",
|
||||||
|
"icon": "http://192.168.1.4:3100/assets/img/favicon.svg",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 9000,
|
||||||
|
"name": "Portainer",
|
||||||
|
"icon": "http://192.168.1.4:9000/63a301f0574f1a696ce6.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 5557,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "./default.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2222,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 5558,
|
||||||
|
"name": "@Joxit",
|
||||||
|
"icon": "./default.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8123,
|
||||||
|
"name": "Home Assistant",
|
||||||
|
"icon": "http://192.168.1.4:8123/static/icons/favicon.ico",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3333,
|
||||||
|
"name": "PairDrop",
|
||||||
|
"icon": "http://192.168.1.4:3333/images/favicon-96x96.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8889,
|
||||||
|
"name": "assets",
|
||||||
|
"icon": "http://192.168.1.4:8889/filebrowser/static/img/icons/favicon-32x32.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4444,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 32400,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 13378,
|
||||||
|
"name": "Audiobookshelf",
|
||||||
|
"icon": "http://192.168.1.4:13378/audiobookshelf/favicon.ico",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8083,
|
||||||
|
"name": "Calibre-Web |..",
|
||||||
|
"icon": "http://192.168.1.4:8083/static/favicon.ico",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3222,
|
||||||
|
"name": "Gitea: Git wi..",
|
||||||
|
"icon": "http://192.168.1.4:3222/assets/img/favicon.svg",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8111,
|
||||||
|
"name": "gpx.studio",
|
||||||
|
"icon": "http://192.168.1.4:8111/android-icon-192x192.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 17777,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8112,
|
||||||
|
"name": "Favycon",
|
||||||
|
"icon": "http://192.168.1.4:8112/favicon-192x192.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8113,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8115,
|
||||||
|
"name": "TileServer GL..",
|
||||||
|
"icon": "./default.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8116,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4533,
|
||||||
|
"name": "Navidrome",
|
||||||
|
"icon": "http://192.168.1.4:4533/android-chrome-192x192.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2342,
|
||||||
|
"name": "PhotoPrism",
|
||||||
|
"icon": "http://192.168.1.4:2342/static/icons/app/1024.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8888,
|
||||||
|
"name": "Photoview",
|
||||||
|
"icon": "http://192.168.1.4:8888/photoview-logo.svg",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3000,
|
||||||
|
"name": "hass-addon-fe",
|
||||||
|
"icon": "http://192.168.1.4:3000/favicon.ico",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3300,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8006,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8200,
|
||||||
|
"name": "Online Markdo..",
|
||||||
|
"icon": "http://192.168.1.4:8200/apple-touch-icon.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 51929,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 9900,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 6080,
|
||||||
|
"name": "noVNC",
|
||||||
|
"icon": "http://192.168.1.4:6080/app/images/icons/novnc-ios-180.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8840,
|
||||||
|
"name": "WatchYourLAN",
|
||||||
|
"icon": "http://192.168.1.4:8840/fs/public/favicon.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4000,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2322,
|
||||||
|
"name": "no_name",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
}
|
||||||
|
]
|
||||||
30
backend/apps.json.orig
Normal file
30
backend/apps.json.orig
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Portainer",
|
||||||
|
"host": "192.168.1.3",
|
||||||
|
"port": 9000,
|
||||||
|
"icon": "http://192.168.1.3:9000/b1f2baef7b5736909c25.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8112,
|
||||||
|
"name": "Favycon",
|
||||||
|
"icon": "http://192.168.1.4:8112/favicon-192x192.png",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3100,
|
||||||
|
"name": "Forgejo",
|
||||||
|
"icon": "http://192.168.1.4:3100/assets/img/favicon.svg",
|
||||||
|
"controllato": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "192.168.1.3",
|
||||||
|
"port": 8081,
|
||||||
|
"name": "mongoDB",
|
||||||
|
"icon": "/icons/error.png",
|
||||||
|
"controllato": true
|
||||||
|
}
|
||||||
|
]
|
||||||
161
backend/apps1.json
Normal file
161
backend/apps1.json
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3100
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 9000
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 5557
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2222
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 5558
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8123
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3333
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8889
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4444
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 32400
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 13378
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8083
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3222
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8111
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 17777
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8112
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8113
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8115
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8116
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4533
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2342
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8888
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3000
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 3300
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8006
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8200
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 51929
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 9900
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 6080
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 8840
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 4000
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"host": "192.168.1.4",
|
||||||
|
"port": 2322
|
||||||
|
}
|
||||||
|
]
|
||||||
87
backend/i.js
Normal file
87
backend/i.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// checkApps.js
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const appsFile = path.join(process.cwd(), "apps.json");
|
||||||
|
|
||||||
|
|
||||||
|
function getShortName($) {
|
||||||
|
return (
|
||||||
|
$('meta[property="og:site_name"]').attr('content') ||
|
||||||
|
$('meta[name="application-name"]').attr('content') ||
|
||||||
|
$('meta[property="og:title"]').attr('content') ||
|
||||||
|
$("title").text().trim() ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function findBestIconAndName(baseUrl) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(baseUrl, { timeout: 2000 });
|
||||||
|
const $ = cheerio.load(res.data);
|
||||||
|
|
||||||
|
//let appName = $("title").text().trim() || null;
|
||||||
|
let appName = getShortName($) || null;
|
||||||
|
const icons = [];
|
||||||
|
$("link[rel*='icon']").each((_, el) => {
|
||||||
|
const href = $(el).attr("href");
|
||||||
|
const sizes = $(el).attr("sizes") || "";
|
||||||
|
if (href) icons.push({ href, sizes });
|
||||||
|
});
|
||||||
|
|
||||||
|
icons.sort((a, b) => {
|
||||||
|
const sizeA = parseInt(a.sizes.split("x")[0]) || 0;
|
||||||
|
const sizeB = parseInt(b.sizes.split("x")[0]) || 0;
|
||||||
|
return sizeB - sizeA;
|
||||||
|
});
|
||||||
|
|
||||||
|
let chosenIcon;
|
||||||
|
if (icons.length > 0) {
|
||||||
|
chosenIcon = new URL(icons[0].href, baseUrl).href;
|
||||||
|
} else {
|
||||||
|
chosenIcon = "./default.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se non c’è nome, metti "no_name"
|
||||||
|
return { name: appName || "no_name", icon: chosenIcon };
|
||||||
|
} catch {
|
||||||
|
return { name: "no_name", icon: "/icons/error.png" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const apps = JSON.parse(fs.readFileSync(appsFile, "utf-8"));
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
if (app.controllato) {
|
||||||
|
console.log(`Skip ${app.name} (${app.host}:${app.port}) già controllato`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `http://${app.host}:${app.port}`;
|
||||||
|
console.log(`Controllo ${baseUrl}...`);
|
||||||
|
|
||||||
|
const result = await findBestIconAndName(baseUrl);
|
||||||
|
|
||||||
|
// Aggiorna nome (anche se "no_name")
|
||||||
|
app.name = result.name;
|
||||||
|
|
||||||
|
// Aggiorna icona
|
||||||
|
app.icon = result.icon;
|
||||||
|
|
||||||
|
// Flag di controllo
|
||||||
|
app.controllato = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(appsFile, JSON.stringify(apps, null, 2));
|
||||||
|
console.log("apps.json aggiornato ✅");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("Errore:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
1330
backend/package-lock.json
generated
Normal file
1330
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
15
backend/package.json
Normal file
15
backend/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "dashboard-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Express server per servire apps.json e frontend build",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/server.js
Normal file
32
backend/server.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import express from "express";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import updateLink from "./updatelink.js";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 4000;
|
||||||
|
|
||||||
|
updateLink().catch((err) => {
|
||||||
|
console.error("Errore:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve lista app da apps.json
|
||||||
|
app.get("/apps", (req, res) => {
|
||||||
|
const apps = JSON.parse(fs.readFileSync(path.join(__dirname, "apps.json")));
|
||||||
|
res.json(apps);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve frontend build
|
||||||
|
app.use(express.static(path.join(__dirname, "../frontend/dist")));
|
||||||
|
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../frontend/dist/index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Dashboard running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
23
backend/server.js.old
Normal file
23
backend/server.js.old
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
const express = require("express");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 4000;
|
||||||
|
|
||||||
|
// Serve lista app da apps.json
|
||||||
|
app.get("/apps", (req, res) => {
|
||||||
|
const apps = JSON.parse(fs.readFileSync(path.join(__dirname, "apps.json")));
|
||||||
|
res.json(apps);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve frontend build
|
||||||
|
app.use(express.static(path.join(__dirname, "../frontend/dist")));
|
||||||
|
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, "../frontend/dist/index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Dashboard running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
122
backend/updatelink.js
Normal file
122
backend/updatelink.js
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// updatelink.js
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const appsFile = path.join(process.cwd(), "apps.json");
|
||||||
|
|
||||||
|
/*function getShortName($) {
|
||||||
|
return (
|
||||||
|
$('meta[property="og:site_name"]').attr("content") ||
|
||||||
|
$('meta[name="application-name"]').attr("content") ||
|
||||||
|
$('meta[property="og:title"]').attr("content") ||
|
||||||
|
$("title").text().trim() ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function limitText(str,n) {
|
||||||
|
if (!str) return "";
|
||||||
|
if (str.length <= n) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.slice(0, n-2) + "..";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShortName($) {
|
||||||
|
const candidates = [
|
||||||
|
/* $('meta[property="og:site_name"]').attr("content"),
|
||||||
|
$('meta[name="application-name"]').attr("content"),
|
||||||
|
$('meta[property="og:title"]').attr("content"),
|
||||||
|
$("title").text().trim(),
|
||||||
|
*/
|
||||||
|
$('meta[property="og:site_name"]').attr("content"),
|
||||||
|
$('meta[name="application-name"]').attr("content"),
|
||||||
|
$('meta[property="og:title"]').attr("content"),
|
||||||
|
$('meta[name="apple-mobile-web-app-title"]').attr("content"),
|
||||||
|
$('meta[name="twitter:title"]').attr("content"),
|
||||||
|
$('meta[name="twitter:site"]').attr("content"),
|
||||||
|
$("title").text().trim(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filtra solo stringhe non nulle e non vuote
|
||||||
|
const valid = candidates.filter(s => s && s.trim().length > 0);
|
||||||
|
|
||||||
|
if (valid.length === 0) return null;
|
||||||
|
|
||||||
|
// Ordina per lunghezza crescente e prende la prima
|
||||||
|
valid.sort((a, b) => a.length - b.length);
|
||||||
|
|
||||||
|
return limitText(valid[0],15);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function consolelogName($) {
|
||||||
|
console.log($('meta[property="og:site_name"]').attr("content"));
|
||||||
|
console.log($('meta[name="application-name"]').attr("content"));
|
||||||
|
console.log($('meta[property="og:title"]').attr("content"));
|
||||||
|
console.log($('meta[name="apple-mobile-web-app-title"]').attr("content"));
|
||||||
|
console.log($('meta[name="twitter:title"]').attr("content"));
|
||||||
|
console.log($('meta[name="twitter:site"]').attr("content"));
|
||||||
|
console.log($("title").text().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findBestIconAndName(baseUrl) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(baseUrl, { timeout: 2000 });
|
||||||
|
const $ = cheerio.load(res.data);
|
||||||
|
|
||||||
|
let appName = getShortName($) || null;
|
||||||
|
consolelogName($);
|
||||||
|
const icons = [];
|
||||||
|
$("link[rel*='icon']").each((_, el) => {
|
||||||
|
const href = $(el).attr("href");
|
||||||
|
const sizes = $(el).attr("sizes") || "";
|
||||||
|
if (href) icons.push({ href, sizes });
|
||||||
|
});
|
||||||
|
|
||||||
|
icons.sort((a, b) => {
|
||||||
|
const sizeA = parseInt(a.sizes.split("x")[0]) || 0;
|
||||||
|
const sizeB = parseInt(b.sizes.split("x")[0]) || 0;
|
||||||
|
return sizeB - sizeA;
|
||||||
|
});
|
||||||
|
|
||||||
|
let chosenIcon;
|
||||||
|
if (icons.length > 0) {
|
||||||
|
chosenIcon = new URL(icons[0].href, baseUrl).href;
|
||||||
|
} else {
|
||||||
|
chosenIcon = "./default.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: appName || "no_name", icon: chosenIcon };
|
||||||
|
} catch {
|
||||||
|
return { name: "no_name", icon: "/icons/error.png" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funzione principale che aggiorna apps.json
|
||||||
|
*/
|
||||||
|
export default async function updateLink() {
|
||||||
|
const apps = JSON.parse(fs.readFileSync(appsFile, "utf-8"));
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
if (app.controllato) {
|
||||||
|
console.log(`Skip ${app.name} (${app.host}:${app.port}) già controllato`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `http://${app.host}:${app.port}`;
|
||||||
|
console.log(`Controllo ${baseUrl}...`);
|
||||||
|
|
||||||
|
const result = await findBestIconAndName(baseUrl);
|
||||||
|
|
||||||
|
app.name = result.name;
|
||||||
|
app.icon = result.icon;
|
||||||
|
app.controllato = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(appsFile, JSON.stringify(apps, null, 2));
|
||||||
|
console.log("apps.json aggiornato ✅");
|
||||||
|
}
|
||||||
40
frontend/dist/assets/index-COhNAj5f.js
vendored
Normal file
40
frontend/dist/assets/index-COhNAj5f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index-DQwn-aJw.css
vendored
Normal file
1
frontend/dist/assets/index-DQwn-aJw.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.app-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:24px;padding:12px}.app-card{display:flex;flex-direction:column;align-items:center;cursor:pointer;padding:16px;border-radius:16px;background-color:#f5f5f5;transition:background-color .2s ease}.app-card:hover{background-color:#e0e0e0}.app-icon{width:160px;height:160px;object-fit:contain;margin-bottom:12px}@media (min-width: 1024px){.app-icon{width:70px;height:70px}}.app-name{font-weight:600;font-size:2rem;text-align:center}@media (min-width: 1024px){.app-name{font-size:1rem}}@tailwind base;@tailwind components;@tailwind utilities;
|
||||||
BIN
frontend/dist/icons/error.png
vendored
Normal file
BIN
frontend/dist/icons/error.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
13
frontend/dist/index.html
vendored
Normal file
13
frontend/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>LAN Dashboard</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-COhNAj5f.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-DQwn-aJw.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>LAN Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/not-applicable.jpg
Normal file
BIN
frontend/not-applicable.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
2101
frontend/package-lock.json
generated
Normal file
2101
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "dashboard-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "React + Vite dashboard per visualizzare app locali",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mui/material": "^7.3.6",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/icons/error.png
Normal file
BIN
frontend/public/icons/error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
74
frontend/src/App.css
Normal file
74
frontend/src/App.css
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/* Griglia adattiva */
|
||||||
|
.app-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@media (min-width: 1200px) {
|
||||||
|
.app-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/* Card stile folder */
|
||||||
|
.app-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icone raddoppiate
|
||||||
|
.app-icon {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
.app-icon {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.app-icon {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Nome raddoppiato e bold
|
||||||
|
.app-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2rem; /* circa 32px */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.app-name {
|
||||||
|
font-size: 1rem; /* circa 19px */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
30
frontend/src/App.jsx
Normal file
30
frontend/src/App.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import "./App.css"; // importiamo lo stile CSS
|
||||||
|
|
||||||
|
function AppGrid() {
|
||||||
|
const [apps, setApps] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/apps")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setApps(Array.isArray(data) ? data : []))
|
||||||
|
.catch(() => setApps([]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-grid">
|
||||||
|
{apps.map(app => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
className="app-card"
|
||||||
|
onClick={() => window.open(`http://${app.host}:${app.port}`, "_blank")}
|
||||||
|
>
|
||||||
|
<img src={app.icon} alt={app.name} className="app-icon" />
|
||||||
|
<div className="app-name">{app.name}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppGrid;
|
||||||
48
frontend/src/App.jsx.old
Normal file
48
frontend/src/App.jsx.old
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function AppGrid() {
|
||||||
|
const [apps, setApps] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/apps")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setApps(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
|
||||||
|
gap: "20px",
|
||||||
|
padding: "20px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{apps.map(app => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
padding: "10px"
|
||||||
|
}}
|
||||||
|
onClick={() => window.open(`http://${app.host}:${app.port}`, "_blank")}
|
||||||
|
>
|
||||||
|
{/* Se app.icon è un URL o un path locale */}
|
||||||
|
<img
|
||||||
|
src={app.icon}
|
||||||
|
alt={app.name}
|
||||||
|
style={{ width: "40px", height: "40px", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
<div style={{ fontWeight: "bold" }}>{app.name}</div>
|
||||||
|
<div style={{ fontSize: "12px", color: "gray" }}>
|
||||||
|
{app.host}:{app.port}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppGrid;
|
||||||
45
frontend/src/App.jsx.old1
Normal file
45
frontend/src/App.jsx.old1
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function AppGrid() {
|
||||||
|
const [apps, setApps] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/apps")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setApps(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{apps.map(app => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "10px" // niente border
|
||||||
|
}}
|
||||||
|
onClick={() => window.open(`http://${app.host}:${app.port}`, "_blank")}
|
||||||
|
>
|
||||||
|
{/* Icona grande il doppio */}
|
||||||
|
<img
|
||||||
|
src={app.icon}
|
||||||
|
alt={app.name}
|
||||||
|
style={{ width: "80px", height: "80px", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
{/* Nome sotto l'icona */}
|
||||||
|
<div style={{ fontWeight: "bold", marginTop: "8px" }}>{app.name}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppGrid;
|
||||||
45
frontend/src/App.jsx.old2
Normal file
45
frontend/src/App.jsx.old2
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function AppGrid() {
|
||||||
|
const [apps, setApps] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/apps")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => setApps(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{apps.map(app => (
|
||||||
|
<div
|
||||||
|
key={app.name}
|
||||||
|
style={{
|
||||||
|
textAlign: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "10px" // niente border
|
||||||
|
}}
|
||||||
|
onClick={() => window.open(`http://${app.host}:${app.port}`, "_blank")}
|
||||||
|
>
|
||||||
|
{/* Icona grande il doppio */}
|
||||||
|
<img
|
||||||
|
src={app.icon}
|
||||||
|
alt={app.name}
|
||||||
|
style={{ width: "80px", height: "80px", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
{/* Nome sotto l'icona */}
|
||||||
|
<div style={{ fontWeight: "bold", marginTop: "8px" }}>{app.name}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppGrid;
|
||||||
BIN
frontend/src/errore.png
Normal file
BIN
frontend/src/errore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import AppGrid from "./App.jsx";
|
||||||
|
import "./index.css"; // importa Tailwind
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<AppGrid />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
25
frontend/tailwind.config.js
Normal file
25
frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// tailwind.config.js
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
screens: {
|
||||||
|
xs: "480px", // breakpoint extra per smartphone piccoli
|
||||||
|
sm: "640px", // tablet piccoli
|
||||||
|
md: "768px", // tablet medi
|
||||||
|
lg: "1024px", // laptop
|
||||||
|
xl: "1280px", // desktop grandi
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
18: "4.5rem", // utile per padding/margin extra
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: "#1e40af", // blu personalizzato per titoli o hover
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: "dist"
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue