implement basic web server for dashboard,

use templates to do basic SSR on head and header
This commit is contained in:
Arthur Lu 2025-02-25 21:35:11 +00:00
parent 84cbe0e45d
commit cfceb32134
21 changed files with 302 additions and 107 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
vars.js **/config.json
**/package-lock.json **/package-lock.json
**/node_modules **/node_modules
dist/*

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: build test clean
build: clean
@echo "======================== Building Binary ======================="
# resolve symbolic links in web by copying it into dist/web/
cp -rL web/ dist/web/
CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/ .
test: clean
go run .
clean:
@echo "======================== Cleaning Project ======================"
go clean
rm -rf dist/*

124
app/app.go Normal file
View File

@ -0,0 +1,124 @@
package app
import (
"flag"
"fmt"
"io/fs"
"log"
"net/http"
embed "proxmoxaas-dashboard/dist/web" // go will complain here until the first build
"text/template"
)
var html map[string]*template.Template
func ParseTemplates() {
html = make(map[string]*template.Template)
fs.WalkDir(embed.HTML, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() { // if it is a html file, parse with all the template files
v, err := fs.ReadFile(embed.HTML, path)
if err != nil {
log.Fatalf("error reading html file %s: %s", path, err.Error())
}
t := template.New(d.Name())
t, err = t.Parse(string(v))
if err != nil {
log.Fatalf("error parsing html file %s: %s", path, err.Error())
}
fs.WalkDir(embed.Templates, ".", func(path string, e fs.DirEntry, err error) error {
if err != nil {
return err
}
if !e.IsDir() { // if it is a template file, parse it
v, err = fs.ReadFile(embed.Templates, path)
if err != nil {
log.Fatalf("error reading template file %s: %s", path, err.Error())
}
t, err = t.Parse(string(v))
if err != nil {
log.Fatalf("error parsing template file %s: %s", path, err.Error())
}
}
return nil
})
html[d.Name()] = t
}
return nil
})
}
func ServeStatic() {
http.Handle("/css/", http.FileServerFS(embed.CSS_fs))
http.Handle("/images/", http.FileServerFS(embed.Images_fs))
http.Handle("/modules/", http.FileServerFS(embed.Modules_fs))
http.Handle("/scripts/", http.FileServerFS(embed.Scripts_fs))
}
func Run() {
configPath := flag.String("config", "config.json", "path to config.json file")
flag.Parse()
global := GetConfig(*configPath)
ParseTemplates()
http.HandleFunc("/account.html", func(w http.ResponseWriter, r *http.Request) {
global.Page = "account"
err := html["account.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
global.Page = "index"
err := html["index.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
http.HandleFunc("/index.html", func(w http.ResponseWriter, r *http.Request) {
global.Page = "index"
err := html["index.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
http.HandleFunc("/instance.html", func(w http.ResponseWriter, r *http.Request) {
global.Page = "instance"
err := html["instance.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
http.HandleFunc("/login.html", func(w http.ResponseWriter, r *http.Request) {
global.Page = "login"
err := html["login.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
http.HandleFunc("/settings.html", func(w http.ResponseWriter, r *http.Request) {
global.Page = "settings"
err := html["settings.html"].Execute(w, global)
if err != nil {
log.Fatal(err.Error())
}
})
ServeStatic()
log.Printf("Starting HTTP server at port: %d\n", global.Port)
err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", global.Port), nil)
if err != nil {
log.Fatal(err)
}
}

28
app/utils.go Normal file
View File

@ -0,0 +1,28 @@
package app
import (
"encoding/json"
"log"
"os"
)
type Config struct {
Port int `json:"listenPort"`
Organization string `json:"organization"`
PVE string `json:"pveurl"`
API string `json:"apiurl"`
Page string
}
func GetConfig(configPath string) Config {
content, err := os.ReadFile(configPath)
if err != nil {
log.Fatal("Error when opening config file: ", err)
}
var config Config
err = json.Unmarshal(content, &config)
if err != nil {
log.Fatal("Error during parsing config file: ", err)
}
return config
}

View File

@ -0,0 +1,8 @@
{
"extends": [
"html-validate:recommended"
],
"rules": {
"no-inline-style": "off"
}
}

View File

@ -0,0 +1,6 @@
{
"listenPort": 8080,
"organization": "myorg",
"apiurl": "https://paas.mydomain.example/api",
"pveurl": "https://pve.mydomain.example"
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module proxmoxaas-dashboard
go 1.23.2

View File

@ -0,0 +1,11 @@
[Unit]
Description=proxmoxaas-dashboard
After=network.target
[Service]
WorkingDirectory=/<path to dir>
ExecStart=/<path to dir>/promoxaas-dashboard
Restart=always
RestartSec=10
Type=simple
[Install]
WantedBy=default.target

View File

@ -4,8 +4,8 @@
"description": "Front-end for ProxmoxAAS", "description": "Front-end for ProxmoxAAS",
"type": "module", "type": "module",
"scripts": { "scripts": {
"lint": "html-validator --continue; stylelint --formatter verbose --fix css/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/", "lint": "html-validate --config configs/.htmlvalidate.json web/html/*; stylelint --config configs/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config configs/.eslintrc.json --fix web/scripts/",
"update-modules": "rm -rf modules/wfa.js modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o modules/wfa.wasm" "update-modules": "rm -rf web/modules/wfa.js web/modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o web/modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o web/modules/wfa.wasm"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.43.0", "eslint": "^8.43.0",
@ -15,6 +15,6 @@
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"stylelint": "^15.9.0", "stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0", "stylelint-config-standard": "^33.0.0",
"w3c-html-validator": "^1.4.0" "html-validate": "^9.4.0"
} }
} }

9
proxmoxaas-dashboard.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
app "proxmoxaas-dashboard/app"
)
func main() {
app.Run()
}

43
web/embed.go Normal file
View File

@ -0,0 +1,43 @@
package embed
import (
"embed"
)
//go:embed css/*
var CSS_fs embed.FS
//go:embed images/*
var Images_fs embed.FS
//go:embed modules/*
var Modules_fs embed.FS
//go:embed scripts/*
var Scripts_fs embed.FS
//go:embed html/*
var HTML embed.FS
//go:embed templates/*
var Templates embed.FS
/*
//go:embed html/account.html
var Account string
//go:embed html/index.html
var Index string
//go:embed html/instance.html
var Instance string
//go:embed html/login.html
var Login string
//go:embed html/settings.html
var Settings string
//go:embed templates/base.html
var Base string
*/

View File

@ -1,15 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> {{template "head" .}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/account.js" type="module"></script> <script src="scripts/account.js" type="module"></script>
<script src="modules/chart.js"></script> <script src="modules/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@ -43,15 +35,7 @@
</head> </head>
<body> <body>
<header> <header>
<h1>{{.Organization}}</h1> {{template "header" .}}
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
<a href="index.html">Instances</a>
<a href="account.html" aria-current="page">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header> </header>
<main> <main>
<h2>Account</h2> <h2>Account</h2>
@ -65,8 +49,8 @@
<section class="w3-card w3-padding"> <section class="w3-card w3-padding">
<div class="flex row nowrap"> <div class="flex row nowrap">
<h3>Password</h3> <h3>Password</h3>
<button class="w3-button w3-margin" id="change-password">Change Password</button> <button class="w3-button w3-margin" id="change-password" type="button">Change Password</button>
</div> </div>
</section> </section>
<section class="w3-card w3-padding"> <section class="w3-card w3-padding">
<h3>Cluster Resources</h3> <h3>Cluster Resources</h3>

View File

@ -1,15 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> {{template "head" .}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/index.js" type="module"></script> <script src="scripts/index.js" type="module"></script>
<script src="modules/wfa.js" type="module"></script> <script src="modules/wfa.js" type="module"></script>
<style> <style>
@ -33,15 +25,7 @@
</head> </head>
<body> <body>
<header> <header>
<h1>{{.Organization}}</h1> {{template "header" .}}
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
<a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header> </header>
<main> <main>
<section> <section>

View File

@ -1,15 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> {{template "head" .}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/instance.js" type="module"></script> <script src="scripts/instance.js" type="module"></script>
<script src="scripts/draggable.js" type="module"></script> <script src="scripts/draggable.js" type="module"></script>
<script src="modules/Sortable.min.js"></script> <script src="modules/Sortable.min.js"></script>
@ -25,15 +17,7 @@
</head> </head>
<body> <body>
<header> <header>
<h1>{{.Organization}}</h1> {{template "header" .}}
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
<a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header> </header>
<main> <main>
<section> <section>

View File

@ -1,25 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> {{template "head" .}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/login.js" type="module"></script> <script src="scripts/login.js" type="module"></script>
</head> </head>
<body> <body>
<header> <header>
<h1>{{.Organization}}</h1> {{template "header" .}}
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
<a href="login.html" aria-current="page">Login</a>
</nav>
</header> </header>
<main class="flex" style="justify-content: center; align-items: center;"> <main class="flex" style="justify-content: center; align-items: center;">
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;"> <div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
@ -32,7 +19,7 @@
<label for="realm">Realm</label> <label for="realm">Realm</label>
<select class="w3-select w3-border" id="realm" name="realm"></select> <select class="w3-select w3-border" id="realm" name="realm"></select>
<div class="w3-center"> <div class="w3-center">
<button class="w3-button w3-margin" id="submit">LOGIN</button> <button class="w3-button w3-margin" id="submit" type="submit">LOGIN</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,15 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> {{template "head" .}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/settings.js" type="module"></script> <script src="scripts/settings.js" type="module"></script>
<style> <style>
legend { legend {
@ -34,19 +26,11 @@
</head> </head>
<body> <body>
<header> <header>
<h1>{{.Organization}}</h1> {{template "header" .}}
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
<a href="index.html">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html" aria-current="page">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header> </header>
<main> <main>
<h2>Settings</h2> <h2>Settings</h2>
<form id = "settings"> <form id="settings">
<section class="w3-card w3-padding"> <section class="w3-card w3-padding">
<h3>Synchronization Settings</h3> <h3>Synchronization Settings</h3>
<fieldset> <fieldset>

View File

@ -1,5 +1,4 @@
import { getSyncSettings, requestAPI } from "./utils.js"; import { getSyncSettings, requestAPI } from "./utils.js";
import { API } from "../vars.js";
export async function setupClientSync (callback) { export async function setupClientSync (callback) {
const { scheme, rate } = getSyncSettings(); const { scheme, rate } = getSyncSettings();
@ -22,7 +21,7 @@ export async function setupClientSync (callback) {
} }
else if (scheme === "interrupt") { else if (scheme === "interrupt") {
callback(); callback();
const socket = new WebSocket(`wss://${API.replace("https://", "")}/sync/interrupt`); const socket = new WebSocket(`wss://${window.API.replace("https://", "")}/sync/interrupt`);
socket.addEventListener("open", (event) => { socket.addEventListener("open", (event) => {
socket.send(`rate ${rate}`); socket.send(`rate ${rate}`);
}); });

View File

@ -2,7 +2,6 @@ import { requestPVE, requestAPI, goToPage, setAppearance, getSearchSettings, goT
import { alert, dialog } from "./dialog.js"; import { alert, dialog } from "./dialog.js";
import { setupClientSync } from "./clientsync.js"; import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js"; import wfaInit from "../modules/wfa.js";
import { PVE } from "../vars.js";
class InstanceCard extends HTMLElement { class InstanceCard extends HTMLElement {
constructor () { constructor () {
@ -220,7 +219,7 @@ class InstanceCard extends HTMLElement {
if (!this.actionLock && this.status === "running") { if (!this.actionLock && this.status === "running") {
const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" }; const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" };
data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1; data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1;
goToURL(PVE, data, true); goToURL(window.PVE, data, true);
} }
} }

View File

@ -1,5 +1,3 @@
import { API } from "../vars.js";
export const resourcesConfig = { export const resourcesConfig = {
cpu: { cpu: {
name: "CPU Type", name: "CPU Type",
@ -224,7 +222,7 @@ export async function requestPVE (path, method, body = null) {
content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken"); content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken");
} }
const response = await request(`${API}/proxmox${path}`, content); const response = await request(`${window.API}/proxmox${path}`, content);
return response; return response;
} }
@ -245,7 +243,7 @@ export async function requestAPI (path, method, body = null) {
content.body = prms.toString(); content.body = prms.toString();
} }
const response = await request(`${API}${path}`, content); const response = await request(`${window.API}${path}`, content);
return response; return response;
} }

View File

@ -1,3 +0,0 @@
export const API = "https://paas.mydomain.example/api"; // the proxmox-aas api
export const PVE = "https://pve.mydomain.example"; // the proxmox api
export const organization = "mydomain"; // org name used in page title and nav bar

31
web/templates/base.html Normal file
View File

@ -0,0 +1,31 @@
{{define "head"}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Organization}} - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<script>
window.PVE = "{{.PVE}}";
window.API = "{{.API}}";
</script>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
{{end}}
{{define "header"}}
<h1>{{.Organization}}</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav id="navigation">
{{if eq .Page "login"}}
<a href="login.html" aria-current="page">Login</a>
{{else}}
<a href="index.html" {{if eq .Page "index"}} aria-current="page" {{end}}>Instances</a>
<a href="account.html" {{if eq .Page "account"}} aria-current="page" {{end}}>Account</a>
<a href="settings.html" {{if eq .Page "settings"}} aria-current="page" {{end}}>Settings</a>
<a href="login.html">Logout</a>
{{end}}
</nav>
{{end}}