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