From 1d0e046ff48110bf39a17b77a3464eae8ff27328 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 5 Jun 2026 16:35:50 +0200 Subject: [PATCH] web: start: add vnc-viewer feature Signed-off-by: GnomeZworc --- .gitignore | 1 + web/build.sh | 67 ++++++++-- web/components.yml | 17 +++ web/components/vnc-viewer/manifest.json | 4 + web/components/vnc-viewer/vnc-viewer.js | 164 ++++++++++++++++++++++++ web/pages/dashboard/dashboard.js | 11 ++ web/pages/dashboard/index.html | 5 + 7 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 web/components/vnc-viewer/manifest.json create mode 100644 web/components/vnc-viewer/vnc-viewer.js diff --git a/.gitignore b/.gitignore index 594473d..aaee194 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ data/ web/components.json web/navigation.json web/pages.json +web/vendor/ diff --git a/web/build.sh b/web/build.sh index 78bc79f..b75143c 100755 --- a/web/build.sh +++ b/web/build.sh @@ -68,9 +68,37 @@ parse_repo_url() { echo "${proto}://${host}" "$owner" "$repo" } +# Build the commits API URL — handles GitHub (api.github.com) vs Gitea (/api/v1). +forge_commits_url() { + local server="$1" owner="$2" repo="$3" ref="$4" + if [[ "$server" == "https://github.com" ]]; then + echo "https://api.github.com/repos/${owner}/${repo}/commits?sha=${ref}&per_page=1" + else + echo "${server}/api/v1/repos/${owner}/${repo}/commits?sha=${ref}&limit=1" + fi +} + +# Build the contents API URL for a given path (empty = repo root). +forge_contents_url() { + local server="$1" owner="$2" repo="$3" ref="$4" path="$5" + if [[ "$server" == "https://github.com" ]]; then + if [[ -z "$path" ]]; then + echo "https://api.github.com/repos/${owner}/${repo}/contents?ref=${ref}" + else + echo "https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}" + fi + else + if [[ -z "$path" ]]; then + echo "${server}/api/v1/repos/${owner}/${repo}/contents?ref=${ref}" + else + echo "${server}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${ref}" + fi + fi +} + resolve_commit() { local server="$1" owner="$2" repo="$3" ref="$4" - api_get "${server}/api/v1/repos/${owner}/${repo}/commits?sha=${ref}&limit=1" \ + api_get "$(forge_commits_url "$server" "$owner" "$repo" "$ref")" \ | jq -r '.[0].sha // empty' } @@ -79,11 +107,7 @@ download_path() { local server="$1" owner="$2" repo="$3" ref="$4" rpath="$5" dest="$6" local api - if [[ -z "$rpath" ]]; then - api="${server}/api/v1/repos/${owner}/${repo}/contents?ref=${ref}" - else - api="${server}/api/v1/repos/${owner}/${repo}/contents/${rpath}?ref=${ref}" - fi + api="$(forge_contents_url "$server" "$owner" "$repo" "$ref" "$rpath")" local listing listing="$(api_get "$api")" @@ -105,8 +129,6 @@ download_path() { done < <(echo "$listing" | jq -c 'if type=="array" then .[] else . end') } -# ── Download WASM libs ─────────────────────────────────────────────────────────── - # ── Read manifest ──────────────────────────────────────────────────────────────── PAGES_DIR="${WEB_DIR}/pages" @@ -115,6 +137,35 @@ mapfile -t LOCALS < <(yq '(.local_components // [])[]' "$MANIFEST") mapfile -t REMOTES < <(yq '(.components // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST") mapfile -t LOCAL_PAGES < <(yq '(.local_pages // [])[]' "$MANIFEST") mapfile -t REMOTE_PAGES < <(yq '(.remote_pages // [])[] | [.name, .repo, (.ref // "main")] | join("|")' "$MANIFEST") +mapfile -t VENDORS < <(yq '(.vendors // [])[] | [.name, .repo, (.ref // "main"), (.path // ""), (.dest // "")] | join("|")' "$MANIFEST") + +# ── Download vendors ──────────────────────────────────────────────────────────── +# Vendors are third-party libraries downloaded into web/vendor/ at build time. +# They are gitignored and never loaded by components.json — components import +# them directly via relative paths (e.g. ../../vendor/novnc/core/rfb.js). + +for spec in "${VENDORS[@]}"; do + [[ -z "$spec" ]] && continue + IFS='|' read -r name repo ref vpath dest <<<"$spec" + [[ -z "$name" || -z "$repo" ]] && die "vendor entry missing name or repo: '$spec'" + + read -r server owner gitrepo <<<"$(parse_repo_url "$repo")" + [[ -z "$dest" ]] && dest="vendor/${name}" + local_dest="${WEB_DIR}/${dest}" + + if [[ "$CHECK_ONLY" == true ]]; then + info "would fetch vendor ${name} from ${repo}@${ref} path=${vpath:-/}" + continue + fi + + log "fetching vendor ${name} ← ${owner}/${gitrepo}@${ref}${vpath:+ path=}${vpath}" + commit="$(resolve_commit "$server" "$owner" "$gitrepo" "$ref")" + [[ -z "$commit" ]] && die "cannot resolve ref '${ref}' in ${owner}/${gitrepo}" + + rm -rf "$local_dest" + download_path "$server" "$owner" "$gitrepo" "$ref" "$vpath" "$local_dest" + info " vendor '${name}' → ${dest} (${commit:0:12})" +done # Ordered list of load paths for components.json declare -a LOAD_PATHS=() diff --git a/web/components.yml b/web/components.yml index e387836..8a885fb 100644 --- a/web/components.yml +++ b/web/components.yml @@ -17,6 +17,7 @@ local_components: - side-panel - date-display - form-vm + - vnc-viewer # Remote components fetched from git at build time. # Each is its own repo; its root must contain .js and manifest.json. @@ -26,6 +27,22 @@ local_components: # ref: v1.2.0 # tag, branch or commit (default: main) components: [] +# Vendor libraries — downloaded into web/vendor/ at build time (gitignored). +# Components import them via relative paths, never via components.json. +# path: restrict download to a subdirectory of the repo (optional). +# dest: destination under web/ (default: vendor/). +vendors: + - name: novnc-core + repo: https://github.com/novnc/noVNC + ref: v1.5.0 + path: core + dest: vendor/novnc/core + - name: novnc-vendor + repo: https://github.com/novnc/noVNC + ref: v1.5.0 + path: vendor + dest: vendor/novnc/vendor + # Pages — each page is a self-contained directory (JS + CSS + manifest.json). # Same model as components: local pages ship in this repo, remote pages are # downloaded from git at build time. build.sh reads each manifest.json to diff --git a/web/components/vnc-viewer/manifest.json b/web/components/vnc-viewer/manifest.json new file mode 100644 index 0000000..7218099 --- /dev/null +++ b/web/components/vnc-viewer/manifest.json @@ -0,0 +1,4 @@ +{ + "tag": "vnc-viewer", + "version": "0.1.0" +} diff --git a/web/components/vnc-viewer/vnc-viewer.js b/web/components/vnc-viewer/vnc-viewer.js new file mode 100644 index 0000000..36c8644 --- /dev/null +++ b/web/components/vnc-viewer/vnc-viewer.js @@ -0,0 +1,164 @@ +// VNC viewer component — wraps noVNC RFB. +// +// Usage (in a side-panel or page): +// +// +// Connects to ws://:9000/vnc/ via the VNC gateway (cmd/vncd). +// Agent host is read from login-gate credentials; falls back to location.hostname. +// +// Attributes: +// vm — VM name (required) +// port — gateway port (default: 9000) + +import RFB from '../../vendor/novnc/core/rfb.js' + +const RECONNECT_DELAY = 3000 // ms between auto-reconnect attempts + +class VncViewer extends HTMLElement { + #rfb = null + #retryTimer = null + + connectedCallback() { + this.attachShadow({ mode: 'open' }) + this.#render() + this.#connect() + } + + disconnectedCallback() { + clearTimeout(this.#retryTimer) + this.#rfb?.disconnect() + this.#rfb = null + } + + // ── Render ────────────────────────────────────────────────────────────────── + + #render() { + this.shadowRoot.innerHTML = ` + + +
+
+ + Connexion… + + + +
+ ` + + this.shadowRoot.getElementById('fullscreen-btn') + .addEventListener('click', () => this.#toggleFullscreen()) + + this.shadowRoot.getElementById('reconnect-btn') + .addEventListener('click', () => { this.#rfb?.disconnect(); this.#connect() }) + } + + // ── Connection ─────────────────────────────────────────────────────────────── + + #wsUrl() { + const vm = this.getAttribute('vm') + const port = this.getAttribute('port') ?? '9000' + const creds = document.querySelector('login-gate')?.credentials + const host = creds?.agent_url + ? new URL(creds.agent_url).hostname + : location.hostname + return `ws://${host}:${port}/vnc/${vm}` + } + + #connect() { + const vm = this.getAttribute('vm') + if (!vm) { this.#setStatus('disconnected', 'Attribut vm manquant'); return } + + const url = this.#wsUrl() + this.#setStatus('connecting', `Connexion à ${url}…`) + + this.#rfb = new RFB(this.shadowRoot.getElementById('screen'), url) + this.#rfb.scaleViewport = true + this.#rfb.resizeSession = false + + this.#rfb.addEventListener('connect', () => { + this.#setStatus('connected', 'Connecté') + }) + + this.#rfb.addEventListener('disconnect', e => { + const clean = e.detail?.clean ?? false + if (clean) { + this.#setStatus('disconnected', 'Déconnecté') + } else { + this.#setStatus('disconnected', 'Déconnecté — reconnexion dans 3s…') + this.#retryTimer = setTimeout(() => this.#connect(), RECONNECT_DELAY) + } + }) + + this.#rfb.addEventListener('credentialsrequired', () => { + this.#rfb.sendCredentials({ password: '' }) + }) + } + + // ── Helpers ────────────────────────────────────────────────────────────────── + + #setStatus(state, msg) { + const dot = this.shadowRoot.getElementById('dot') + const text = this.shadowRoot.getElementById('status-text') + if (!dot || !text) return + dot.className = `dot ${state}` + text.textContent = msg + } + + #toggleFullscreen() { + const screen = this.shadowRoot.getElementById('screen') + if (!document.fullscreenElement) { + screen.requestFullscreen?.() + } else { + document.exitFullscreen?.() + } + } +} + +customElements.define('vnc-viewer', VncViewer) diff --git a/web/pages/dashboard/dashboard.js b/web/pages/dashboard/dashboard.js index 773ec8f..9df2f71 100644 --- a/web/pages/dashboard/dashboard.js +++ b/web/pages/dashboard/dashboard.js @@ -38,6 +38,17 @@ export function init(main) { }) }) + main.querySelectorAll('[data-action="console"]').forEach(btn => { + btn.addEventListener('click', () => { + SidePanel.open({ + title: 'afficher une consol', + width: '700px', + content: `` + }) + // form-vm gère son propre submit et dispatche vm-saved — pas besoin de querySelector + }) + }) + // vm-saved est attaché sur main (pas document) → nettoyé automatiquement // quand le router remplace le contenu de
main.addEventListener('vm-saved', e => { diff --git a/web/pages/dashboard/index.html b/web/pages/dashboard/index.html index ba114f1..9e8195c 100644 --- a/web/pages/dashboard/index.html +++ b/web/pages/dashboard/index.html @@ -21,4 +21,9 @@ padding:7px 14px;color:#a6e3a1;font-size:13px;cursor:pointer;"> Edition +