initial commit

This commit is contained in:
2025-09-16 09:25:23 +02:00
commit 0746cc4296
43 changed files with 13336 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
# Proxmox
PROXMOX_API_URL=https://proxmox.example/api2/json
PROXMOX_USER=root@pam
PROXMOX_PASSWORD=secret
# Check_MK
CHECK_MK_API_URL=https://checkmk.example
CHECK_MK_USER=automation
CHECK_MK_PASSWORD=secret
VERIFY_TLS=false

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# environment variables
.env
# virtual environment
.venv/
# python excludes
__pycache__/
__pypackages__/
*.py[cod]
*$py.class
*.pyo
*.pyd
# distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
# cache
cache/
# github
.github/instructions/

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"python.testing.pytestArgs": ["tests"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

36
Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
FROM python:3.11-slim
# Set a working directory
WORKDIR /app
# Prevent Python from writing .pyc files and enable unbuffered stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
APP_PORT=8081 \
GUNICORN_CMD_ARGS="--bind=0.0.0.0:${APP_PORT} --workers=4 --threads=2"
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user
RUN useradd --create-home --shell /bin/bash appuser
# Copy requirements and install
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . /app
RUN chown -R appuser:appuser /app
USER appuser
# expose the default APP_PORT (can be overridden at runtime)
EXPOSE 8081
# Default command: run the app with gunicorn
CMD ["gunicorn", "--chdir", "./", "app:app"]

96
README.md Normal file
View File

@@ -0,0 +1,96 @@
# LAN Web Overview
This project is a web application that uses Flask to provide an interactive interface to view proxmox servers, virtual machines, and other devices monitored by check_mk.
It is designed to run on a local area network (LAN) and provides a StarTrek-themed interface for easy navigation and monitoring.
## Features
- View proxmox servers and their virtual machines
- View devices monitored by check_mk and their services
- Interactive and user-friendly interface
- StarTrek-themed design for a unique user experience
- Responsive layout for various screen sizes
## Requirements
- Python 3.x
- Flask
- Requests
## Installation (Development Setup)
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Navigate to the project directory:
```bash
cd lan-web
```
3. Install the required dependencies:
```bash
pip install -r requirements.txt
```
4. Set up environment variables for configuration (e.g., PROXMOX_API_URL, CHECK_MK_API_URL, etc.).
5. Run the Flask application:
```bash
python app.py
```
6. Open your web browser and navigate to `http://localhost:5000`.
## Installation (Docker Setup)
1. Ensure you have Docker installed on your machine.
2. Clone the repository:
```bash
git clone <repository-url>
```
3. Navigate to the project directory:
```bash
cd lan-web
```
4. Build the Docker image:
```bash
docker build -t lan-web .
```
5. Run the Docker container (the application listens on APP_PORT, default 8081):
```bash
# default image runs gunicorn bound to port 8081
docker run -d -p 8081:8081 --env-file .env -e APP_PORT=8081 lan-web
```
6. Alternatively, use docker-compose (recommended for development). You can set APP_PORT in your `.env` file or use the default 8081:
```bash
# with default APP_PORT
APP_PORT=8081 docker compose up --build -d
```
7. Open your web browser and navigate to `http://localhost:8081` (or the port you configured via APP_PORT).
## Configuration
The application requires configuration through environment variables. Copy the sample `.env.example` file in the project root to a new file named `.env` and fill in the required values.
## Usage
1. Start the application using either the development setup or Docker setup.
2. Navigate to the web interface in your browser.
3. Use the navigation menu to explore proxmox servers, virtual machines, and check_mk monitored devices.
4. Click on individual items to view detailed information and status.
5. Use the search functionality to quickly find specific servers or devices.

133
app.py Normal file
View File

@@ -0,0 +1,133 @@
import random
import requests
import json
import os
from flask import Flask, render_template, abort
from utils.proxmox_client import ProxmoxClient
from utils.check_mk_client import CheckMKClient
from config import settings
app = Flask(__name__)
def make_client(client_cls, base_url: str):
"""Generic factory to construct API clients with TLS and auth from settings."""
return client_cls(
base_url,
user=settings.CHECK_MK_USER if client_cls is CheckMKClient else settings.PROXMOX_USER,
password=settings.CHECK_MK_PASSWORD if client_cls is CheckMKClient else settings.PROXMOX_PASSWORD,
api_token=(
settings.CHECK_MK_API_TOKEN if client_cls is CheckMKClient else settings.PROXMOX_API_TOKEN) or None,
verify=settings.VERIFY_TLS,
ca_bundle=settings.CA_BUNDLE or None,
)
proxmox = make_client(ProxmoxClient, settings.PROXMOX_API_URL)
checkmk = make_client(CheckMKClient, settings.CHECK_MK_API_URL)
@app.route('/')
def index():
# gather cluster hosts and VMs
try:
cluster = proxmox.get_cluster()
except Exception as e:
return render_template('error.html', error=str(e)), 500
# enrich hosts with check_mk status
hosts = []
for node in cluster.get('nodes', []):
host = {
'name': node.get('name'),
'status': node.get('status'),
# convert to GB
'memory': round((node.get('memory') or node.get('maxmem') or 0)/1024/1024/1024, 2),
'cpu': node.get('cpu') or node.get('maxcpu') or 0,
'vm_count': len(node.get('qemu', [])) if node.get('qemu') else 0,
'lxc_count': len(node.get('lxc', [])) if node.get('lxc') else 0,
}
try:
host['check_mk'] = checkmk.get_host_status(host['name'])
except Exception:
host['check_mk'] = None
hosts.append(host)
return render_template('index.html', hosts=hosts)
@app.route('/host/<hostname>')
def host_detail(hostname):
# get services for host from check_mk
try:
services = checkmk.get_host_services(hostname)
except Exception as e:
return render_template('error.html', error=str(e)), 500
return render_template('host_detail.html', hostname=hostname, services=services)
@app.route('/service/<path:url>')
def service(url: str):
print(f"Received URL parameter: {url}")
# urldecode service_url
service_url = url.encode('utf-8').decode('unicode_escape')
print(f"Decoded service URL: {service_url}")
client = make_client(CheckMKClient, settings.CHECK_MK_API_URL)
# fetch service detail from check_mk
try:
request = client.get_service_detail(service_url)
ret = request
except Exception as e:
return render_template('error.html', error=str(e)), 500
return render_template('service_detail.html', service=ret)
@app.route('/numbers')
def numbers():
# generates numbers for the data cascade design
n = 216 # total numbers to generate
# n = 24 * 12
# n = 9*7
min_len = 1
max_len = 7
letter_list = ['A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'K', 'L', 'S', 'T', 'U', 'X', 'Y', 'Z']
number_list = []
for i in range(1, n + 1):
prefix_length = random.randint(0, 2)
prefix = ''
for k in range(0, prefix_length):
prefix += random.choice(letter_list)
if prefix_length > 0 and random.choice([True, False]):
prefix += '-'
else:
prefix = ''
max_num = 10**(max_len - len(prefix)) - 1
if max_num < min_len:
max_num = min_len
number = random.randint(1, max_num)
number_list.append(f"{prefix}{number}")
return json.dumps(number_list)
@app.route('/favicon.ico')
def favicon():
svg = """
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect width="100" height="100" fill="rgb(204, 153, 255)" />
</svg>
"""
return svg, 200, {'Content-Type': 'image/svg+xml'}
if __name__ == '__main__':
# configurable port via APP_PORT env var; default to 8081
try:
APP_PORT = int(os.getenv('APP_PORT', '8081'))
except Exception:
APP_PORT = 8081
app.run(host='0.0.0.0', port=APP_PORT, debug=True)

27
config.py Normal file
View File

@@ -0,0 +1,27 @@
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass
class Settings:
PROXMOX_API_URL: str = os.environ.get(
'PROXMOX_API_URL', 'https://proxmox.example/api2/json')
PROXMOX_USER: str = os.environ.get('PROXMOX_USER', '')
PROXMOX_PASSWORD: str = os.environ.get('PROXMOX_PASSWORD', '')
PROXMOX_API_TOKEN: str = os.environ.get('PROXMOX_API_TOKEN', '')
CHECK_MK_API_URL: str = os.environ.get(
'CHECK_MK_API_URL', 'https://checkmk.example')
CHECK_MK_USER: str = os.environ.get('CHECK_MK_USER', '')
CHECK_MK_PASSWORD: str = os.environ.get('CHECK_MK_PASSWORD', '')
CHECK_MK_API_TOKEN: str = os.environ.get('CHECK_MK_API_TOKEN', '')
# TLS verification controls
VERIFY_TLS: bool = os.environ.get(
'VERIFY_TLS', 'true').lower() in ('1', 'true', 'yes')
CA_BUNDLE: str = os.environ.get('CA_BUNDLE', '')
settings = Settings()

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
version: "3.8"
services:
lan-web:
build: .
image: lan-web:latest
restart: unless-stopped
ports:
- "${APP_PORT:-8081}:${APP_PORT:-8081}"
env_file:
- .env
volumes:
- ./:/app:ro
healthcheck:
test:
["CMD-SHELL", "curl -f http://localhost:${APP_PORT:-8081}/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Flask>=2.0
requests>=2.25
python-dotenv>=0.19
pytest>=7.0
gunicorn>=20.1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
document.addEventListener("touchstart", function() {},false);
let mybutton = document.getElementById("topBtn");
window.onscroll = function() {scrollFunction()};
function scrollFunction() {
if (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) {
mybutton.style.display = "block";
} else {
mybutton.style.display = "none";
}
}
function topFunction() {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
function playSoundAndRedirect(audioId, url) {
var audio = document.getElementById(audioId);
audio.play();
audio.onended = function() {
window.location.href = url;
};
}
function goToAnchor(anchorId) {
window.location.hash = anchorId;
}
// Accordion drop-down
var acc = document.getElementsByClassName("accordion");
var i;
for (i = 0; i < acc.length; i++) {
acc[i].addEventListener("click", function() {
this.classList.toggle("active");
var accordionContent = this.nextElementSibling;
if (accordionContent.style.maxHeight){
accordionContent.style.maxHeight = null;
} else {
accordionContent.style.maxHeight = accordionContent.scrollHeight + "px";
}
});
}
// LCARS keystroke sound (not to be used with hyperlinks)
const LCARSkeystroke = document.getElementById('LCARSkeystroke');
const allPlaySoundButtons = document.querySelectorAll('.playSoundButton');
allPlaySoundButtons.forEach(button => {
button.addEventListener('click', function() {
LCARSkeystroke.pause();
LCARSkeystroke.currentTime = 0; // Reset to the beginning of the sound
LCARSkeystroke.play();
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,380 @@
<!DOCTYPE html>
<html>
<head>
<title>Classic Starndard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/classic.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<section class="wrap-standard" id="column-3">
<div class="wrap">
<div class="left-frame-top">
<!--
*** LCARS PANEL BUTTON ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<div class="panel-2">02<span class="hop">-262000</span></div>
</div>
<div class="right-frame-top">
<div class="banner">LCARS &#149; 47988</div>
<div class="data-cascade-button-group">
<div class="data-cascade-wrapper" id="default">
<div class="data-column">
<div class="dc-row-1">93</div>
<div class="dc-row-1">1853</div>
<div class="dc-row-2">24109</div>
<div class="dc-row-3">7</div>
<div class="dc-row-3">7024</div>
<div class="dc-row-4">322</div>
<div class="dc-row-5">4149</div>
<div class="dc-row-6">86</div>
<div class="dc-row-7">05</div>
</div>
<div class="data-column">
<div class="dc-row-1">21509</div>
<div class="dc-row-1">68417</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3">2048</div>
<div class="dc-row-3">319825</div>
<div class="dc-row-4">46233</div>
<div class="dc-row-5">05</div>
<div class="dc-row-6">2014</div>
<div class="dc-row-7">30986</div>
</div>
<div class="data-column">
<div class="dc-row-1">585101</div>
<div class="dc-row-1">25403</div>
<div class="dc-row-2">31219</div>
<div class="dc-row-3">752</div>
<div class="dc-row-3">0604</div>
<div class="dc-row-4">21048</div>
<div class="dc-row-5">293612</div>
<div class="dc-row-6">534082</div>
<div class="dc-row-7">206</div>
</div>
<div class="data-column">
<div class="dc-row-1">2107853</div>
<div class="dc-row-1">12201972</div>
<div class="dc-row-2">24487255</div>
<div class="dc-row-3">30412</div>
<div class="dc-row-3">98</div>
<div class="dc-row-4">4024161</div>
<div class="dc-row-5">888</div>
<div class="dc-row-6">35045462</div>
<div class="dc-row-7">41520257</div>
</div>
<div class="data-column">
<div class="dc-row-1">33</div>
<div class="dc-row-1">56</div>
<div class="dc-row-2">04</div>
<div class="dc-row-3">69</div>
<div class="dc-row-3">41</div>
<div class="dc-row-4">15</div>
<div class="dc-row-5">25</div>
<div class="dc-row-6">65</div>
<div class="dc-row-7">21</div>
</div>
<div class="data-column">
<div class="dc-row-1">0223</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">28471</div>
<div class="dc-row-3">21366</div>
<div class="dc-row-3">8654</div>
<div class="dc-row-4">31</div>
<div class="dc-row-5">1984</div>
<div class="dc-row-6">272</div>
<div class="dc-row-7">21854</div>
</div>
<div class="data-column">
<div class="dc-row-1">633</div>
<div class="dc-row-1">51166</div>
<div class="dc-row-2">41699</div>
<div class="dc-row-3">6188</div>
<div class="dc-row-3">15033</div>
<div class="dc-row-4">21094</div>
<div class="dc-row-5">32881</div>
<div class="dc-row-6">26083</div>
<div class="dc-row-7">2143</div>
</div>
<div class="data-column">
<div class="dc-row-1">406822</div>
<div class="dc-row-1">81205</div>
<div class="dc-row-2">91007</div>
<div class="dc-row-3">38357</div>
<div class="dc-row-3">110</div>
<div class="dc-row-4">2041</div>
<div class="dc-row-5">312</div>
<div class="dc-row-6">57104</div>
<div class="dc-row-7">00708</div>
</div>
<div class="data-column">
<div class="dc-row-1">12073</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">21982</div>
<div class="dc-row-3">20254</div>
<div class="dc-row-3">55</div>
<div class="dc-row-4">38447</div>
<div class="dc-row-5">26921</div>
<div class="dc-row-6">285</div>
<div class="dc-row-7">30102</div>
</div>
<div class="data-column">
<div class="dc-row-1">21604</div>
<div class="dc-row-1">15421</div>
<div class="dc-row-2">25</div>
<div class="dc-row-3">3808</div>
<div class="dc-row-3">582031</div>
<div class="dc-row-4">62311</div>
<div class="dc-row-5">85799</div>
<div class="dc-row-6">87</div>
<div class="dc-row-7">6895</div>
</div>
<div class="data-column">
<div class="dc-row-1">72112</div>
<div class="dc-row-1">101088</div>
<div class="dc-row-2">604122</div>
<div class="dc-row-3">126523</div>
<div class="dc-row-3">86801</div>
<div class="dc-row-4">8447</div>
<div class="dc-row-5">210486</div>
<div class="dc-row-6">LV426</div>
<div class="dc-row-7">220655</div>
</div>
<div class="data-column">
<div class="dc-row-1">272448</div>
<div class="dc-row-1">29620</div>
<div class="dc-row-2">339048</div>
<div class="dc-row-3">31802</div>
<div class="dc-row-3">9859</div>
<div class="dc-row-4">672304</div>
<div class="dc-row-5">581131</div>
<div class="dc-row-6">338</div>
<div class="dc-row-7">70104</div>
</div>
<div class="data-column">
<div class="dc-row-1">16182</div>
<div class="dc-row-1">711632</div>
<div class="dc-row-2">102955</div>
<div class="dc-row-3">2061</div>
<div class="dc-row-3">5804</div>
<div class="dc-row-4">850233</div>
<div class="dc-row-5">833441</div>
<div class="dc-row-6">465</div>
<div class="dc-row-7">210047</div>
</div>
<div class="data-column">
<div class="dc-row-1">75222</div>
<div class="dc-row-1">98824</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3">858552</div>
<div class="dc-row-3">696730</div>
<div class="dc-row-4">307124</div>
<div class="dc-row-5">58414</div>
<div class="dc-row-6">209</div>
<div class="dc-row-7">808044</div>
</div>
<div class="data-column">
<div class="dc-row-1">331025</div>
<div class="dc-row-1">62118</div>
<div class="dc-row-2">2700</div>
<div class="dc-row-3">395852</div>
<div class="dc-row-3">604206</div>
<div class="dc-row-4">26</div>
<div class="dc-row-5">309150</div>
<div class="dc-row-6">885</div>
<div class="dc-row-7">210411</div>
</div>
<div class="data-column">
<div class="dc-row-1">817660</div>
<div class="dc-row-1">121979</div>
<div class="dc-row-2">20019</div>
<div class="dc-row-3">462869</div>
<div class="dc-row-3">25002</div>
<div class="dc-row-4">308</div>
<div class="dc-row-5">52074</div>
<div class="dc-row-6">33</div>
<div class="dc-row-7">80544</div>
</div>
<div class="data-column">
<div class="dc-row-1">1070</div>
<div class="dc-row-1">020478</div>
<div class="dc-row-2">26419</div>
<div class="dc-row-3">372122</div>
<div class="dc-row-3">2623</div>
<div class="dc-row-4">79</div>
<div class="dc-row-5">90008</div>
<div class="dc-row-6">8049</div>
<div class="dc-row-7">251664</div>
</div>
<div class="data-column">
<div class="dc-row-1">900007</div>
<div class="dc-row-1">704044</div>
<div class="dc-row-2">982365</div>
<div class="dc-row-3">25819</div>
<div class="dc-row-3">385</div>
<div class="dc-row-4">656214</div>
<div class="dc-row-5">409</div>
<div class="dc-row-6">218563</div>
<div class="dc-row-7">527222</div>
</div>
<div class="data-column">
<div class="dc-row-1">80106</div>
<div class="dc-row-1">1314577</div>
<div class="dc-row-2">39001</div>
<div class="dc-row-3">7162893</div>
<div class="dc-row-3">12855</div>
<div class="dc-row-4">57</div>
<div class="dc-row-5">23966</div>
<div class="dc-row-6">4</div>
<div class="dc-row-7">6244009</div>
</div>
<div class="data-column">
<div class="dc-row-1">2352</div>
<div class="dc-row-1">308</div>
<div class="dc-row-2">928</div>
<div class="dc-row-3">2721</div>
<div class="dc-row-3">8890</div>
<div class="dc-row-4">402</div>
<div class="dc-row-5">540</div>
<div class="dc-row-6">795</div>
<div class="dc-row-7">23</div>
</div>
<div class="data-column">
<div class="dc-row-1">66880</div>
<div class="dc-row-1">8675309</div>
<div class="dc-row-2">821533</div>
<div class="dc-row-3">249009</div>
<div class="dc-row-3">51922</div>
<div class="dc-row-4">600454</div>
<div class="dc-row-5">9035768</div>
<div class="dc-row-6">453571</div>
<div class="dc-row-7">825064</div>
</div>
<div class="data-column">
<div class="dc-row-1">131488</div>
<div class="dc-row-1">641212</div>
<div class="dc-row-2">218035</div>
<div class="dc-row-3">37</div>
<div class="dc-row-3">6022</div>
<div class="dc-row-4">82</div>
<div class="dc-row-5">572104</div>
<div class="dc-row-6">799324</div>
<div class="dc-row-7">4404</div>
</div>
<div class="data-column">
<div class="dc-row-1">8807</div>
<div class="dc-row-1">4481</div>
<div class="dc-row-2">8915</div>
<div class="dc-row-3">2104</div>
<div class="dc-row-3">1681</div>
<div class="dc-row-4">326</div>
<div class="dc-row-5">446</div>
<div class="dc-row-6">8337</div>
<div class="dc-row-7">526</div>
</div>
<div class="data-column">
<div class="dc-row-1">593</div>
<div class="dc-row-1">8057</div>
<div class="dc-row-2">22</div>
<div class="dc-row-3">23</div>
<div class="dc-row-3">6722</div>
<div class="dc-row-4">890</div>
<div class="dc-row-5">2608</div>
<div class="dc-row-6">7274</div>
<div class="dc-row-7">2103</div>
</div>
</div> <!-- /data-cascade-wrapper -->
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<div class="bar-5"></div>
</div>
</div>
</div>
<div class="wrap" id="gap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
<div class="panel-7">07<span class="hop">-081940</span></div>
<div class="panel-8">08<span class="hop">-47148</span></div>
<div class="panel-9">09<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-10">10<span class="hop">-31</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
<div class="bar-10"></div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Classic Theme &#149; Standard Layout</h2>
<h3 class="font-gold">Version 24.2</h3>
<h4>Replace This Content With Your Own</h4>
<p class="go-big">Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content &copy; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</section>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

View File

@@ -0,0 +1,465 @@
<!DOCTYPE html>
<html>
<head>
<title>Classic Ultra</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/classic.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<div class="wrap-everything">
<section id="column-1">
<div class="lcars-frame">
<div class="frame-col-1">
<div class="frame-col-1-cell-a"></div>
<div class="frame-col-1-cell-b"></div>
<div class="frame-col-1-cell-c"></div>
</div>
<div class="frame-col-2"> </div>
<div class="frame-col-3 display-vertical">
<div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div>
</div>
<div class="frame-col-4"> </div>
<div class="frame-col-5">
<div class="frame-col-5-cell-a"></div>
<div class="frame-col-5-cell-b"></div>
<div class="frame-col-5-cell-c"></div>
</div>
</div>
<div class="pillbox">
<!--
*** ULTRA LAYOUT SECTION 1 PILL BUTTONS ***
Replace the hashtag '#' in each button with a real url (or not). If you don't want sound effects for these links, replace buttons with <a> tags like this:
<a href="#">J-001</a>
<a href="#">R-002</a>
<a href="#">R-003</a>
<a href="#">I-004</a>
<a href="#">C-005</a>
<a href="#">A-006</a>
-->
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">J-001</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">R-002</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">R-003</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">I-004</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">C-005</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">A-006</button>
</div>
<div class="lcars-list-2 uppercase">
<ul>
<li>Subspace Link: Established</li>
<li>Starfleet Database: Connected</li>
<li>Quantum Memory Field: stable</li>
<li class="bullet-almond-creme font-almond-creme">Optical Data Network: rerouting</li>
</ul>
</div>
<div class="pillbox-2">
<!--
*** ULTRA LAYOUT SECTION 1 PILL BUTTONS SET 2 ***
Replace the hashtag '#' in each button with a real url (or not). If you don't want sound effects for these links, replace buttons with <a> tags like this:
<a href="#">F12-22</a>
<a href="#">G24-22</a>
<div class="pill-2"></div>
<a href="#">H-07AM</a>
<a href="#">I50-72</a>
<a href="#">J5369</a>
-->
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">F12-22</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">G24-22</button>
<div class="pill-2"> </div>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">H-07AM</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">I50-72</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">J5369</button>
</div>
</section>
<section id="column-2">
<div class="panel-11"> 11-1524 </div>
<!--
*** ULTRA LAYOUT SECTION 2 SIDEBAR BUTTONS ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tags. If you don't want sound effects for these links, replace the <button> elements with the following <div> + <a> elements:
<div class="section-2-buttons">
<a href="">JS2B-01</a>
<a href="">IS2B-02</a>
<a href="">MS2B-03</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-almond-creme">JS2B-01</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-butterscotch">JS2B-02</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-african-violet">MS2B-03</button>
<div class="panel-12"> 12-0730</div>
<div class="panel-13">13-318</div>
<div class="panel-14">14-DL44</div>
<div class="panel-15">15-3504</div>
</section>
<section id="column-3">
<div class="wrap">
<div class="left-frame-top">
<!--
*** LCARS PANEL BUTTON ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<div class="panel-2">02<span class="hop">-262000</span></div>
</div>
<div class="right-frame-top">
<div class="banner"> LCARS &#149; 47988 </div>
<div class="data-cascade-button-group">
<div class="data-cascade-wrapper" id="default">
<div class="data-column">
<div class="dc-row-1">93</div>
<div class="dc-row-1">1853</div>
<div class="dc-row-2">24109</div>
<div class="dc-row-3">7</div>
<div class="dc-row-3">7024</div>
<div class="dc-row-4">322</div>
<div class="dc-row-5">4149</div>
<div class="dc-row-6">86</div>
<div class="dc-row-7">05</div>
</div>
<div class="data-column">
<div class="dc-row-1">21509</div>
<div class="dc-row-1">68417</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3">2048</div>
<div class="dc-row-3">319825</div>
<div class="dc-row-4">46233</div>
<div class="dc-row-5">05</div>
<div class="dc-row-6">2014</div>
<div class="dc-row-7">30986</div>
</div>
<div class="data-column">
<div class="dc-row-1">585101</div>
<div class="dc-row-1">25403</div>
<div class="dc-row-2">31219</div>
<div class="dc-row-3">752</div>
<div class="dc-row-3">0604</div>
<div class="dc-row-4">21048</div>
<div class="dc-row-5">293612</div>
<div class="dc-row-6">534082</div>
<div class="dc-row-7">206</div>
</div>
<div class="data-column">
<div class="dc-row-1">2107853</div>
<div class="dc-row-1">12201972</div>
<div class="dc-row-2">24487255</div>
<div class="dc-row-3">30412</div>
<div class="dc-row-3">98</div>
<div class="dc-row-4">4024161</div>
<div class="dc-row-5">888</div>
<div class="dc-row-6">35045462</div>
<div class="dc-row-7">41520257</div>
</div>
<div class="data-column">
<div class="dc-row-1">33</div>
<div class="dc-row-1">56</div>
<div class="dc-row-2">04</div>
<div class="dc-row-3">69</div>
<div class="dc-row-3">41</div>
<div class="dc-row-4">15</div>
<div class="dc-row-5">25</div>
<div class="dc-row-6">65</div>
<div class="dc-row-7">21</div>
</div>
<div class="data-column">
<div class="dc-row-1">0223</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">28471</div>
<div class="dc-row-3">21366</div>
<div class="dc-row-3">8654</div>
<div class="dc-row-4">31</div>
<div class="dc-row-5">1984</div>
<div class="dc-row-6">272</div>
<div class="dc-row-7">21854</div>
</div>
<div class="data-column">
<div class="dc-row-1">633</div>
<div class="dc-row-1">51166</div>
<div class="dc-row-2">41699</div>
<div class="dc-row-3">6188</div>
<div class="dc-row-3">15033</div>
<div class="dc-row-4">21094</div>
<div class="dc-row-5">32881</div>
<div class="dc-row-6">26083</div>
<div class="dc-row-7">2143</div>
</div>
<div class="data-column">
<div class="dc-row-1">406822</div>
<div class="dc-row-1">81205</div>
<div class="dc-row-2">91007</div>
<div class="dc-row-3">38357</div>
<div class="dc-row-3">110</div>
<div class="dc-row-4">2041</div>
<div class="dc-row-5">312</div>
<div class="dc-row-6">57104</div>
<div class="dc-row-7">00708</div>
</div>
<div class="data-column">
<div class="dc-row-1">12073</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">21982</div>
<div class="dc-row-3">20254</div>
<div class="dc-row-3">55</div>
<div class="dc-row-4">38447</div>
<div class="dc-row-5">26921</div>
<div class="dc-row-6">285</div>
<div class="dc-row-7">30102</div>
</div>
<div class="data-column">
<div class="dc-row-1">21604</div>
<div class="dc-row-1">15421</div>
<div class="dc-row-2">25</div>
<div class="dc-row-3">3808</div>
<div class="dc-row-3">582031</div>
<div class="dc-row-4">62311</div>
<div class="dc-row-5">85799</div>
<div class="dc-row-6">87</div>
<div class="dc-row-7">6895</div>
</div>
<div class="data-column">
<div class="dc-row-1">72112</div>
<div class="dc-row-1">101088</div>
<div class="dc-row-2">604122</div>
<div class="dc-row-3">126523</div>
<div class="dc-row-3">86801</div>
<div class="dc-row-4">8447</div>
<div class="dc-row-5">210486</div>
<div class="dc-row-6">LV426</div>
<div class="dc-row-7">220655</div>
</div>
<div class="data-column">
<div class="dc-row-1">272448</div>
<div class="dc-row-1">29620</div>
<div class="dc-row-2">339048</div>
<div class="dc-row-3">31802</div>
<div class="dc-row-3">9859</div>
<div class="dc-row-4">672304</div>
<div class="dc-row-5">581131</div>
<div class="dc-row-6">338</div>
<div class="dc-row-7">70104</div>
</div>
<div class="data-column">
<div class="dc-row-1">16182</div>
<div class="dc-row-1">711632</div>
<div class="dc-row-2">102955</div>
<div class="dc-row-3">2061</div>
<div class="dc-row-3">5804</div>
<div class="dc-row-4">850233</div>
<div class="dc-row-5">833441</div>
<div class="dc-row-6">465</div>
<div class="dc-row-7">210047</div>
</div>
<div class="data-column">
<div class="dc-row-1">75222</div>
<div class="dc-row-1">98824</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3">858552</div>
<div class="dc-row-3">696730</div>
<div class="dc-row-4">307124</div>
<div class="dc-row-5">58414</div>
<div class="dc-row-6">209</div>
<div class="dc-row-7">808044</div>
</div>
<div class="data-column">
<div class="dc-row-1">331025</div>
<div class="dc-row-1">62118</div>
<div class="dc-row-2">2700</div>
<div class="dc-row-3">395852</div>
<div class="dc-row-3">604206</div>
<div class="dc-row-4">26</div>
<div class="dc-row-5">309150</div>
<div class="dc-row-6">885</div>
<div class="dc-row-7">210411</div>
</div>
<div class="data-column">
<div class="dc-row-1">817660</div>
<div class="dc-row-1">121979</div>
<div class="dc-row-2">20019</div>
<div class="dc-row-3">462869</div>
<div class="dc-row-3">25002</div>
<div class="dc-row-4">308</div>
<div class="dc-row-5">52074</div>
<div class="dc-row-6">33</div>
<div class="dc-row-7">80544</div>
</div>
<div class="data-column">
<div class="dc-row-1">1070</div>
<div class="dc-row-1">020478</div>
<div class="dc-row-2">26419</div>
<div class="dc-row-3">372122</div>
<div class="dc-row-3">2623</div>
<div class="dc-row-4">79</div>
<div class="dc-row-5">90008</div>
<div class="dc-row-6">8049</div>
<div class="dc-row-7">251664</div>
</div>
<div class="data-column">
<div class="dc-row-1">900007</div>
<div class="dc-row-1">704044</div>
<div class="dc-row-2">982365</div>
<div class="dc-row-3">25819</div>
<div class="dc-row-3">385</div>
<div class="dc-row-4">656214</div>
<div class="dc-row-5">409</div>
<div class="dc-row-6">218563</div>
<div class="dc-row-7">527222</div>
</div>
<div class="data-column">
<div class="dc-row-1">80106</div>
<div class="dc-row-1">1314577</div>
<div class="dc-row-2">39001</div>
<div class="dc-row-3">7162893</div>
<div class="dc-row-3">12855</div>
<div class="dc-row-4">57</div>
<div class="dc-row-5">23966</div>
<div class="dc-row-6">4</div>
<div class="dc-row-7">6244009</div>
</div>
<div class="data-column">
<div class="dc-row-1">2352</div>
<div class="dc-row-1">308</div>
<div class="dc-row-2">928</div>
<div class="dc-row-3">2721</div>
<div class="dc-row-3">8890</div>
<div class="dc-row-4">402</div>
<div class="dc-row-5">540</div>
<div class="dc-row-6">795</div>
<div class="dc-row-7">23</div>
</div>
<div class="data-column">
<div class="dc-row-1">66880</div>
<div class="dc-row-1">8675309</div>
<div class="dc-row-2">821533</div>
<div class="dc-row-3">249009</div>
<div class="dc-row-3">51922</div>
<div class="dc-row-4">600454</div>
<div class="dc-row-5">9035768</div>
<div class="dc-row-6">453571</div>
<div class="dc-row-7">825064</div>
</div>
<div class="data-column">
<div class="dc-row-1">131488</div>
<div class="dc-row-1">641212</div>
<div class="dc-row-2">218035</div>
<div class="dc-row-3">37</div>
<div class="dc-row-3">6022</div>
<div class="dc-row-4">82</div>
<div class="dc-row-5">572104</div>
<div class="dc-row-6">799324</div>
<div class="dc-row-7">4404</div>
</div>
<div class="data-column">
<div class="dc-row-1">8807</div>
<div class="dc-row-1">4481</div>
<div class="dc-row-2">8915</div>
<div class="dc-row-3">2104</div>
<div class="dc-row-3">1681</div>
<div class="dc-row-4">326</div>
<div class="dc-row-5">446</div>
<div class="dc-row-6">8337</div>
<div class="dc-row-7">526</div>
</div>
<div class="data-column">
<div class="dc-row-1">593</div>
<div class="dc-row-1">8057</div>
<div class="dc-row-2">22</div>
<div class="dc-row-3">23</div>
<div class="dc-row-3">6722</div>
<div class="dc-row-4">890</div>
<div class="dc-row-5">2608</div>
<div class="dc-row-6">7274</div>
<div class="dc-row-7">2103</div>
</div>
</div> <!-- /data-cascade-wrapper -->
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<div class="bar-5"></div>
</div>
</div>
</div>
<div class="wrap" id="gap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is styled like a panel in the sidebar and appears at the bottom of the page after scrolling down. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
<div class="panel-7">07<span class="hop">-081940</span></div>
<div class="panel-8">08<span class="hop">-47148</span></div>
<div class="panel-9">09<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-10">10<span class="hop">-31</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
<div class="bar-10"></div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Classic Theme &#149; Ultra Layout</h2>
<h3 class="font-gold">Version 24.2</h3>
<h4>Replace This Content With Your Own</h4>
<p class="go-big">Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content &copy; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</section>
</div>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html>
<head>
<title>Lower Decks PADD</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/lower-decks-padd.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<div class="wrap-all">
<div class="wrap">
<div class="left-frame-top">
<!--
*** LCARS PANEL BUTTON ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<div class="panel-2">02<span class="hop">-262000</span></div>
</div>
<div class="right-frame-top">
<div class="banner">LCARS 57436.2</div>
<div class="data-cascade-button-group">
<div class="data-wrapper">
<div class="data-column">
<div class="dc-row-1 font-arctic-ice">47</div>
<div class="dc-row-2">31</div>
<div class="dc-row-3">28</div>
<div class="dc-row-4">94</div>
</div>
<div class="data-column">
<div class="dc-row-1">329</div>
<div class="dc-row-2 font-night-rain">128</div>
<div class="dc-row-3">605</div>
<div class="dc-row-4">704</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-night-rain">39725514862</div>
<div class="dc-row-2 font-arctic-ice">51320259663</div>
<div class="dc-row-3 font-alpha-blue">21857221984</div>
<div class="dc-row-4">40372566301</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-arctic-ice">56</div>
<div class="dc-row-2 font-night-rain">04</div>
<div class="dc-row-3 font-night-rain">40</div>
<div class="dc-row-4 font-night-rain">35</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-arctic-ice">614</div>
<div class="dc-row-2 font-arctic-ice">883</div>
<div class="dc-row-3 font-alpha-blue">109</div>
<div class="dc-row-4">297</div>
</div>
<div class="data-column">
<div class="dc-row-1 darkspace darkfont">000</div>
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
<div class="dc-row-4 darkspace font-night-rain">25</div>
</div>
<div class="data-column">
<div class="dc-row-1">48</div>
<div class="dc-row-2 font-night-rain">07</div>
<div class="dc-row-3">38</div>
<div class="dc-row-4">62</div>
</div>
<div class="data-column">
<div class="dc-row-1">416</div>
<div class="dc-row-2 font-night-rain">001</div>
<div class="dc-row-3">888</div>
<div class="dc-row-4">442</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-night-rain">86225514862</div>
<div class="dc-row-2 font-arctic-ice">31042009183</div>
<div class="dc-row-3 font-alpha-blue">74882306985</div>
<div class="dc-row-4">54048523421</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-alpha-blue">10</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3 font-night-rain">31</div>
<div class="dc-row-4 font-alpha-blue">85</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-alpha-blue">87</div>
<div class="dc-row-2">71</div>
<div class="dc-row-3 font-night-rain">40</div>
<div class="dc-row-4 font-night-rain">26</div>
</div>
<div class="data-column">
<div class="dc-row-1">98</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3 font-night-rain">52</div>
<div class="dc-row-4 font-alpha-blue">71</div>
</div>
<div class="data-column">
<div class="dc-row-1">118</div>
<div class="dc-row-2">270</div>
<div class="dc-row-3">395</div>
<div class="dc-row-4">260</div>
</div>
<div class="data-column">
<div class="dc-row-1">8675309</div>
<div class="dc-row-2 font-night-rain">7952705</div>
<div class="dc-row-3">9282721</div>
<div class="dc-row-4">4981518</div>
</div>
<div class="data-column">
<div class="dc-row-1 darkspace darkfont">000</div>
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
<div class="dc-row-4 darkspace font-night-rain">84</div>
</div>
<div class="data-column">
<div class="dc-row-1">65821407321</div>
<div class="dc-row-2 font-alpha-blue">54018820533</div>
<div class="dc-row-3 font-night-rain">27174523016</div>
<div class="dc-row-4">38954062564</div>
</div>
<div class="data-column">
<div class="dc-row-1 font-arctic-ice">999</div>
<div class="dc-row-2 font-arctic-ice">202</div>
<div class="dc-row-3 font-alpha-blue">574</div>
<div class="dc-row-4">293</div>
</div>
<div class="data-column">
<div class="dc-row-1">3872</div>
<div class="dc-row-2 font-night-rain">1105</div>
<div class="dc-row-3">1106</div>
<div class="dc-row-4 font-alpha-blue">7411</div>
</div>
</div>
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"> </div>
<div class="bar-2"> </div>
<div class="bar-3"> </div>
<div class="bar-4"> </div>
<div class="bar-5"> </div>
</div>
</div>
</div>
<div class="divider">
<div class="block-left"> </div>
<div class="block-right">
<div class="block-row">
<div class="bar-11"> </div>
<div class="bar-12"> </div>
<div class="bar-13"> </div>
<div class="bar-14">
<div class="blockhead"> </div>
</div>
</div>
</div>
</div>
<div class="wrap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
</div>
<div>
<div class="panel-7">07<span class="hop">-081940</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"> </div>
<div class="bar-7"> </div>
<div class="bar-8"> </div>
<div class="bar-9"> </div>
<div class="bar-10"> </div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Lower Decks PADD Theme</h2>
<h3 class="font-radioactive">Version 24.2</h3>
<h4>Replace This Content With Your Own</h4>
<p class="go-big">Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content Copyright &#169; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</div>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

View File

@@ -0,0 +1,238 @@
<!DOCTYPE html>
<html>
<head>
<title>Lower Decks</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/lower-decks.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<div class="wrap-all">
<div class="wrap">
<div class="scroll-top"><a id="scroll-top" href=""><span class="hop">screen</span> top</a></div>
<div class="left-frame-top">
<!--
*** LCARS PANEL BUTTON ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<div class="panel-2">02<span class="hop">-262000</span></div>
</div>
<div class="right-frame-top">
<div class="banner"> <a href="">LCARS</a> 2380</div>
<div class="data-cascade-button-group">
<div class="data-wrapper">
<div class="data-column">
<div class="dc-row-1">03</div>
<div class="dc-row-2">69</div>
<div class="dc-row-3">84</div>
<div class="dc-row-4">54</div>
</div>
<div class="data-column">
<div class="dc-row-1">416</div>
<div class="dc-row-2">508</div>
<div class="dc-row-3">752</div>
<div class="dc-row-4">629</div>
</div>
<div class="data-column">
<div class="dc-row-1">397<span class="hide-data">25514862</span></div>
<div class="dc-row-2">513<span class="hide-data">20259663</span></div>
<div class="dc-row-3">218<span class="hide-data">57221984</span></div>
<div class="dc-row-4">403<span class="hide-data">72566301</span></div>
</div>
<div class="data-column">
<div class="dc-row-1">56</div>
<div class="dc-row-2">04</div>
<div class="dc-row-3">40</div>
<div class="dc-row-4">35</div>
</div>
<div class="data-column">
<div class="dc-row-1">61</div>
<div class="dc-row-2">68</div>
<div class="dc-row-3">47</div>
<div class="dc-row-4">29</div>
</div>
<div class="data-column">
<div class="dc-row-1 darkspace darkfont">0</div>
<div class="dc-row-2 darkspace">21</div>
<div class="dc-row-3 darkspace">79</div>
<div class="dc-row-4 darkspace darkfont">0</div>
</div>
<div class="data-column">
<div class="dc-row-1">81</div>
<div class="dc-row-2">07</div>
<div class="dc-row-3">38</div>
<div class="dc-row-4">62</div>
</div>
<div class="data-column">
<div class="dc-row-1">416</div>
<div class="dc-row-2">001</div>
<div class="dc-row-3">888</div>
<div class="dc-row-4">442</div>
</div>
<div class="data-column">
<div class="dc-row-1">86225514862</div>
<div class="dc-row-2">31042009183</div>
<div class="dc-row-3">74882306985</div>
<div class="dc-row-4">54048523421</div>
</div>
<div class="data-column">
<div class="dc-row-1">10</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3">31</div>
<div class="dc-row-4">85</div>
</div>
<div class="data-column">
<div class="dc-row-1">87</div>
<div class="dc-row-2">71</div>
<div class="dc-row-3">40</div>
<div class="dc-row-4">26</div>
</div>
<div class="data-column">
<div class="dc-row-1 darkspace darkfont">0</div>
<div class="dc-row-2 darkspace">56</div>
<div class="dc-row-3 darkspace">28</div>
<div class="dc-row-4 darkspace darkfont">0</div>
</div>
<div class="data-column">
<div class="dc-row-1">98</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3">52</div>
<div class="dc-row-4">71</div>
</div>
<div class="data-column">
<div class="dc-row-1">118</div>
<div class="dc-row-2">270</div>
<div class="dc-row-3">395</div>
<div class="dc-row-4">260</div>
</div>
<div class="data-column">
<div class="dc-row-1">65821407321</div>
<div class="dc-row-2">54018820533</div>
<div class="dc-row-3">27174523016</div>
<div class="dc-row-4">38954062564</div>
</div>
<div class="data-column">
<div class="dc-row-1 darkspace darkfont">0</div>
<div class="dc-row-2 darkspace">99</div>
<div class="dc-row-3 darkspace">10</div>
<div class="dc-row-4 darkspace darkfont">0</div>
</div>
<div class="data-column">
<div class="dc-row-1">31</div>
<div class="dc-row-2">20</div>
<div class="dc-row-3">57</div>
<div class="dc-row-4">12</div>
</div>
<div class="data-column">
<div class="dc-row-1">119</div>
<div class="dc-row-2">570</div>
<div class="dc-row-3">333</div>
<div class="dc-row-4">402</div>
</div>
<div class="data-column">
<div class="dc-row-1">8675309</div>
<div class="dc-row-2">7952705</div>
<div class="dc-row-3">9282721</div>
<div class="dc-row-4">4981518</div>
</div>
<div class="data-column">
<div class="dc-row-1">38</div>
<div class="dc-row-2">62</div>
<div class="dc-row-3">97</div>
<div class="dc-row-4">42</div>
</div>
<div class="data-column">
<div class="dc-row-1">562</div>
<div class="dc-row-2">139</div>
<div class="dc-row-3">716</div>
<div class="dc-row-4">573</div>
</div>
</div>
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
</div>
</div>
</div>
<div class="wrap" id="gap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<div class="panel-4">04<span class="hop">-41969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-7">7<span class="hop">-31</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Lower Decks Theme</h2>
<h3 class="font-october-sunset">Version 24.2</h3>
<h4>Replace This Content With Your Own</h4>
<p class="go-big">Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content Copyright &#169; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</div>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

View File

@@ -0,0 +1,394 @@
<!DOCTYPE html>
<html>
<head>
<title>Nemesis Blue Standard</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/nemesis-blue.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<section class="wrap-standard" id="column-3">
<div class="wrap">
<div class="left-frame-top">
<!--
*** TOP PANEL BUTTONS ***
Replace the hashtag '#' with a real URL (or not) in the <button> tags following this comment. If you do not want a sound effect for this link, replace the <button> elements with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
<div class="panel-2">
<a href="#">02<span class="hop">-262000</span></a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-moonbeam">02<span class="hop">-262000</span></button>
</div>
<div class="right-frame-top">
<div class="banner">LCARS &#149; 56844</div>
<div class="data-cascade-button-group">
<div class="data-cascade-wrapper" id="default">
<div class="data-column">
<div class="dc-row-1">93</div>
<div class="dc-row-1">1853</div>
<div class="dc-row-2">24109</div>
<div class="dc-row-3">7</div>
<div class="dc-row-3">7024</div>
<div class="dc-row-4">322</div>
<div class="dc-row-5">4149</div>
<div class="dc-row-6">86</div>
<div class="dc-row-7">05</div>
</div>
<div class="data-column">
<div class="dc-row-1">21509</div>
<div class="dc-row-1">68417</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3">2048</div>
<div class="dc-row-3">319825</div>
<div class="dc-row-4">46233</div>
<div class="dc-row-5">05</div>
<div class="dc-row-6">2014</div>
<div class="dc-row-7">30986</div>
</div>
<div class="data-column">
<div class="dc-row-1">585101</div>
<div class="dc-row-1">25403</div>
<div class="dc-row-2">31219</div>
<div class="dc-row-3">752</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">21048</div>
<div class="dc-row-5">293612</div>
<div class="dc-row-6">534082</div>
<div class="dc-row-7">206</div>
</div>
<div class="data-column">
<div class="dc-row-1">2107853</div>
<div class="dc-row-1">12201972</div>
<div class="dc-row-2">24487255</div>
<div class="dc-row-3">30412</div>
<div class="dc-row-3">98</div>
<div class="dc-row-4">4024161</div>
<div class="dc-row-5">888</div>
<div class="dc-row-6">35045462</div>
<div class="dc-row-7">41520257</div>
</div>
<div class="data-column">
<div class="dc-row-1">33</div>
<div class="dc-row-1">56</div>
<div class="dc-row-2">04</div>
<div class="dc-row-3">69</div>
<div class="dc-row-3">41</div>
<div class="dc-row-4">15</div>
<div class="dc-row-5">25</div>
<div class="dc-row-6">65</div>
<div class="dc-row-7">21</div>
</div>
<div class="data-column">
<div class="dc-row-1">0223</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">28471</div>
<div class="dc-row-3">21366</div>
<div class="dc-row-3">8654</div>
<div class="dc-row-4">31</div>
<div class="dc-row-5">1984</div>
<div class="dc-row-6">272</div>
<div class="dc-row-7">21854</div>
</div>
<div class="data-column">
<div class="dc-row-1">633</div>
<div class="dc-row-1">51166</div>
<div class="dc-row-2">41699</div>
<div class="dc-row-3">6188</div>
<div class="dc-row-3">15033</div>
<div class="dc-row-4">21094</div>
<div class="dc-row-5">32881</div>
<div class="dc-row-6">26083</div>
<div class="dc-row-7">2143</div>
</div>
<div class="data-column">
<div class="dc-row-1">406822</div>
<div class="dc-row-1">81205</div>
<div class="dc-row-2">91007</div>
<div class="dc-row-3">38357</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">2041</div>
<div class="dc-row-5">312</div>
<div class="dc-row-6">57104</div>
<div class="dc-row-7">00708</div>
</div>
<div class="data-column">
<div class="dc-row-1">12073</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">21982</div>
<div class="dc-row-3">20254</div>
<div class="dc-row-3">55</div>
<div class="dc-row-4">38447</div>
<div class="dc-row-5">26921</div>
<div class="dc-row-6">285</div>
<div class="dc-row-7">30102</div>
</div>
<div class="data-column">
<div class="dc-row-1">21604</div>
<div class="dc-row-1">15421</div>
<div class="dc-row-2">25</div>
<div class="dc-row-3">3808</div>
<div class="dc-row-3">582031</div>
<div class="dc-row-4">62311</div>
<div class="dc-row-5">85799</div>
<div class="dc-row-6">87</div>
<div class="dc-row-7">6895</div>
</div>
<div class="data-column">
<div class="dc-row-1">72112</div>
<div class="dc-row-1">101088</div>
<div class="dc-row-2">604122</div>
<div class="dc-row-3">126523</div>
<div class="dc-row-3">86801</div>
<div class="dc-row-4">8447</div>
<div class="dc-row-5">210486</div>
<div class="dc-row-6">LV426</div>
<div class="dc-row-7">220655</div>
</div>
<div class="data-column">
<div class="dc-row-1">272448</div>
<div class="dc-row-1">296520</div>
<div class="dc-row-2">339048</div>
<div class="dc-row-3">31802</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">672304</div>
<div class="dc-row-5">581131</div>
<div class="dc-row-6">338</div>
<div class="dc-row-7">70104</div>
</div>
<div class="data-column">
<div class="dc-row-1">16182</div>
<div class="dc-row-1">711632</div>
<div class="dc-row-2">102955</div>
<div class="dc-row-3">2061</div>
<div class="dc-row-3">5804</div>
<div class="dc-row-4">850233</div>
<div class="dc-row-5">833441</div>
<div class="dc-row-6">465</div>
<div class="dc-row-7">210047</div>
</div>
<div class="data-column">
<div class="dc-row-1">75222</div>
<div class="dc-row-1">98824</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3">858552</div>
<div class="dc-row-3">696730</div>
<div class="dc-row-4">307124</div>
<div class="dc-row-5">58414</div>
<div class="dc-row-6">209</div>
<div class="dc-row-7">808044</div>
</div>
<div class="data-column">
<div class="dc-row-1">331025</div>
<div class="dc-row-1">62118</div>
<div class="dc-row-2">2700</div>
<div class="dc-row-3">395852</div>
<div class="dc-row-3">604206</div>
<div class="dc-row-4">26</div>
<div class="dc-row-5">309150</div>
<div class="dc-row-6">885</div>
<div class="dc-row-7">210411</div>
</div>
<div class="data-column">
<div class="dc-row-1">817660</div>
<div class="dc-row-1">121979</div>
<div class="dc-row-2">20019</div>
<div class="dc-row-3">462869</div>
<div class="dc-row-3">25002</div>
<div class="dc-row-4">308</div>
<div class="dc-row-5">52074</div>
<div class="dc-row-6">33</div>
<div class="dc-row-7">80544</div>
</div>
<div class="data-column">
<div class="dc-row-1">1070</div>
<div class="dc-row-1">020478</div>
<div class="dc-row-2">26419</div>
<div class="dc-row-3">372122</div>
<div class="dc-row-3">2623</div>
<div class="dc-row-4">79</div>
<div class="dc-row-5">90008</div>
<div class="dc-row-6">8049</div>
<div class="dc-row-7">251664</div>
</div>
<div class="data-column">
<div class="dc-row-1">900007</div>
<div class="dc-row-1">704044</div>
<div class="dc-row-2">982365</div>
<div class="dc-row-3">258819</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">656214</div>
<div class="dc-row-5">409</div>
<div class="dc-row-6">218563</div>
<div class="dc-row-7">527222</div>
</div>
<div class="data-column">
<div class="dc-row-1">80106</div>
<div class="dc-row-1">1314577</div>
<div class="dc-row-2">39001</div>
<div class="dc-row-3">7162893</div>
<div class="dc-row-3">12855</div>
<div class="dc-row-4">57</div>
<div class="dc-row-5">23966</div>
<div class="dc-row-6">4</div>
<div class="dc-row-7">6244009</div>
</div>
<div class="data-column">
<div class="dc-row-1">2352</div>
<div class="dc-row-1">308</div>
<div class="dc-row-2">928</div>
<div class="dc-row-3">2721</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">402</div>
<div class="dc-row-5">540</div>
<div class="dc-row-6">795</div>
<div class="dc-row-7">23</div>
</div>
<div class="data-column">
<div class="dc-row-1">66880</div>
<div class="dc-row-1">8675309</div>
<div class="dc-row-2">821533</div>
<div class="dc-row-3">249009</div>
<div class="dc-row-3">51922</div>
<div class="dc-row-4">600454</div>
<div class="dc-row-5">9035768</div>
<div class="dc-row-6">453571</div>
<div class="dc-row-7">825064</div>
</div>
<div class="data-column">
<div class="dc-row-1">131488</div>
<div class="dc-row-1">641212</div>
<div class="dc-row-2">218035</div>
<div class="dc-row-3">37</div>
<div class="dc-row-3">6022</div>
<div class="dc-row-4">82</div>
<div class="dc-row-5">572104</div>
<div class="dc-row-6">799324</div>
<div class="dc-row-7">4404</div>
</div>
<div class="data-column">
<div class="dc-row-1">8807</div>
<div class="dc-row-1">4481</div>
<div class="dc-row-2">8915</div>
<div class="dc-row-3">2104</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">326</div>
<div class="dc-row-5">446</div>
<div class="dc-row-6">8337</div>
<div class="dc-row-7">526</div>
</div>
<div class="data-column">
<div class="dc-row-1">593</div>
<div class="dc-row-1">8057</div>
<div class="dc-row-2">22</div>
<div class="dc-row-3">23</div>
<div class="dc-row-3">6722</div>
<div class="dc-row-4">890</div>
<div class="dc-row-5">2608</div>
<div class="dc-row-6">7274</div>
<div class="dc-row-7">2103</div>
</div>
</div>
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="floor-text">
optical data network available
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<div class="bar-5"></div>
</div>
</div>
</div>
<div class="wrap" id="gap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is styled like a panel in the sidebar and appears at the bottom of the page after scrolling down. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<!-- <div class="sidebar-buttons">
<a href="">1-042</a>
<a href="">2-079</a>
<a href="">3-184</a>
<a href="">4-033</a>
<a href="">5-216</a>
<a href="">6-315</a>
</div> -->
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
<div class="panel-7">07<span class="hop">-081940</span></div>
<div class="panel-8">08<span class="hop">-47148</span></div>
<div class="panel-9">09<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-10">10<span class="hop">-31</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
<div class="bar-10"></div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Nemesis Blue Theme &#149; Standard Layout</h2>
<h3 class="font-grape">Version 24.2</h3>
<h4>Replace this content with your own.</h4>
<p>Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content &copy; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</section>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

View File

@@ -0,0 +1,479 @@
<!DOCTYPE html>
<html>
<head>
<title>Nemesis Blue Ultra</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="format-detection" content="telephone=no">
<meta name="format-detection" content="date=no">
<link rel="stylesheet" type="text/css" href="assets/nemesis-blue.css">
</head>
<body>
<audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio>
<div class="wrap-everything">
<section id="column-1">
<div class="lcars-frame">
<div class="frame-col-1">
<div class="frame-col-1-cell-a"></div>
<div class="frame-col-1-cell-b"></div>
<div class="frame-col-1-cell-c"></div>
</div>
<div class="frame-col-2"> </div>
<div class="frame-col-3 display-horizontal">
<div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div><div class="line"></div>
</div>
<div class="frame-col-4"> </div>
<div class="frame-col-5">
<div class="frame-col-5-cell-a"></div>
<div class="frame-col-5-cell-b"></div>
<div class="frame-col-5-cell-c"></div>
</div>
</div>
<div class="pillbox">
<!--
*** ULTRA LAYOUT SECTION 1 PILL BUTTONS ***
Replace the hashtag '#' in each button with a real url (or not). If you don't want sound effects for these links, replace buttons with <a> tags like this:
<a href="#">J-001</a>
<a href="#">R-002</a>
<a href="#">R-003</a>
<a href="#">I-004</a>
<a href="#">C-005</a>
<a href="#">A-006</a>
-->
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">J-001</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">R-002</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">R-003</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">I-004</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">C-005</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill">A-006</button>
</div>
<div class="lcars-list-2 uppercase">
<ul>
<li>Subspace Link: Established</li>
<li>Starfleet Database: Connected</li>
<li>Quantum Memory Field: stable</li>
<li class="bullet-moonbeam font-moonbeam">Optical Data Network: <span class="blink">rerouting</span></li>
</ul>
</div>
<div class="pillbox-2">
<!--
*** ULTRA LAYOUT SECTION 1 PILL BUTTONS SET 2 ***
Replace the hashtag '#' in each button with a real url (or not). If you don't want sound effects for these links, replace buttons with <a> tags like this:
<a href="#">F12-22</a>
<a href="#">G24-22</a>
<div class="pill-2"></div>
<a href="#">H-07AM</a>
<a href="#">I50-72</a>
<a href="#">J5369</a>
-->
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">F12-22</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">G24-22</button>
<div class="pill-2"> </div>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">H-07AM</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">I50-72</button>
<button onclick="playSoundAndRedirect('audio1', '#')" class="pill-2">J5369</button>
</div>
</section>
<section id="column-2">
<div class="panel-11"> 11-1524 </div>
<!--
*** ULTRA LAYOUT SECTION 2 SIDEBAR BUTTONS ***
Replace the hashtag '#' with a real URL (or not) in the following <button> tags. If you don't want sound effects for these links, replace the <button> elements with the following <div> + <a> elements:
<div class="section-2-buttons">
<a href="#">JS2B-01</a>
<a href="#">IS2B-02</a>
<a href="#">MS2B-03</a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-evening">JS2B-01</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-moonbeam">JS2B-02</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-evening">MS2B-03</button>
<div class="panel-12"> 12-0730</div>
<!-- The next button is a standalone <button> and is not contained in the parent "section-2-buttons" div. If you don't want a sound effect, replace the <button> element with this div + <a> tag:
<div class="panel-13"><a href="#">13-318</a></div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-honey">13-318</button>
<div class="panel-14">14-DL44</div>
<div class="panel-15">15-3504</div>
</section>
<section id="column-3">
<div class="wrap">
<div class="left-frame-top">
<!--
*** TOP PANEL BUTTONS ***
Replace the hashtag '#' with a real URL (or not) in the <button> tags following this comment. If you do not want a sound effect for this link, replace the <button> elements with the following <div> + <a> elements:
<div class="panel-1">
<a href="#">LCARS</a>
</div>
<div class="panel-2">
<a href="#">02<span class="hop">-262000</span></a>
</div>
-->
<button onclick="playSoundAndRedirect('audio2', '#')" class="panel-1-button">LCARS</button>
<button onclick="playSoundAndRedirect('audio2', '#')" class="sidebar-button button-moonbeam">02<span class="hop">-262000</span></button>
</div>
<div class="right-frame-top">
<div class="banner"> LCARS &#149; 56844 </div>
<div class="data-cascade-button-group">
<div class="data-cascade-wrapper" id="default">
<div class="data-column">
<div class="dc-row-1">93</div>
<div class="dc-row-1">1853</div>
<div class="dc-row-2">24109</div>
<div class="dc-row-3">7</div>
<div class="dc-row-3">7024</div>
<div class="dc-row-4">322</div>
<div class="dc-row-5">4149</div>
<div class="dc-row-6">86</div>
<div class="dc-row-7">05</div>
</div>
<div class="data-column">
<div class="dc-row-1">21509</div>
<div class="dc-row-1">68417</div>
<div class="dc-row-2">80</div>
<div class="dc-row-3">2048</div>
<div class="dc-row-3">319825</div>
<div class="dc-row-4">46233</div>
<div class="dc-row-5">05</div>
<div class="dc-row-6">2014</div>
<div class="dc-row-7">30986</div>
</div>
<div class="data-column">
<div class="dc-row-1">585101</div>
<div class="dc-row-1">25403</div>
<div class="dc-row-2">31219</div>
<div class="dc-row-3">752</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">21048</div>
<div class="dc-row-5">293612</div>
<div class="dc-row-6">534082</div>
<div class="dc-row-7">206</div>
</div>
<div class="data-column">
<div class="dc-row-1">2107853</div>
<div class="dc-row-1">12201972</div>
<div class="dc-row-2">24487255</div>
<div class="dc-row-3">30412</div>
<div class="dc-row-3">98</div>
<div class="dc-row-4">4024161</div>
<div class="dc-row-5">888</div>
<div class="dc-row-6">35045462</div>
<div class="dc-row-7">41520257</div>
</div>
<div class="data-column">
<div class="dc-row-1">33</div>
<div class="dc-row-1">56</div>
<div class="dc-row-2">04</div>
<div class="dc-row-3">69</div>
<div class="dc-row-3">41</div>
<div class="dc-row-4">15</div>
<div class="dc-row-5">25</div>
<div class="dc-row-6">65</div>
<div class="dc-row-7">21</div>
</div>
<div class="data-column">
<div class="dc-row-1">0223</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">28471</div>
<div class="dc-row-3">21366</div>
<div class="dc-row-3">8654</div>
<div class="dc-row-4">31</div>
<div class="dc-row-5">1984</div>
<div class="dc-row-6">272</div>
<div class="dc-row-7">21854</div>
</div>
<div class="data-column">
<div class="dc-row-1">633</div>
<div class="dc-row-1">51166</div>
<div class="dc-row-2">41699</div>
<div class="dc-row-3">6188</div>
<div class="dc-row-3">15033</div>
<div class="dc-row-4">21094</div>
<div class="dc-row-5">32881</div>
<div class="dc-row-6">26083</div>
<div class="dc-row-7">2143</div>
</div>
<div class="data-column">
<div class="dc-row-1">406822</div>
<div class="dc-row-1">81205</div>
<div class="dc-row-2">91007</div>
<div class="dc-row-3">38357</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">2041</div>
<div class="dc-row-5">312</div>
<div class="dc-row-6">57104</div>
<div class="dc-row-7">00708</div>
</div>
<div class="data-column">
<div class="dc-row-1">12073</div>
<div class="dc-row-1">688</div>
<div class="dc-row-2">21982</div>
<div class="dc-row-3">20254</div>
<div class="dc-row-3">55</div>
<div class="dc-row-4">38447</div>
<div class="dc-row-5">26921</div>
<div class="dc-row-6">285</div>
<div class="dc-row-7">30102</div>
</div>
<div class="data-column">
<div class="dc-row-1">21604</div>
<div class="dc-row-1">15421</div>
<div class="dc-row-2">25</div>
<div class="dc-row-3">3808</div>
<div class="dc-row-3">582031</div>
<div class="dc-row-4">62311</div>
<div class="dc-row-5">85799</div>
<div class="dc-row-6">87</div>
<div class="dc-row-7">6895</div>
</div>
<div class="data-column">
<div class="dc-row-1">72112</div>
<div class="dc-row-1">101088</div>
<div class="dc-row-2">604122</div>
<div class="dc-row-3">126523</div>
<div class="dc-row-3">86801</div>
<div class="dc-row-4">8447</div>
<div class="dc-row-5">210486</div>
<div class="dc-row-6">LV426</div>
<div class="dc-row-7">220655</div>
</div>
<div class="data-column">
<div class="dc-row-1">272448</div>
<div class="dc-row-1">296520</div>
<div class="dc-row-2">339048</div>
<div class="dc-row-3">31802</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">672304</div>
<div class="dc-row-5">581131</div>
<div class="dc-row-6">338</div>
<div class="dc-row-7">70104</div>
</div>
<div class="data-column">
<div class="dc-row-1">16182</div>
<div class="dc-row-1">711632</div>
<div class="dc-row-2">102955</div>
<div class="dc-row-3">2061</div>
<div class="dc-row-3">5804</div>
<div class="dc-row-4">850233</div>
<div class="dc-row-5">833441</div>
<div class="dc-row-6">465</div>
<div class="dc-row-7">210047</div>
</div>
<div class="data-column">
<div class="dc-row-1">75222</div>
<div class="dc-row-1">98824</div>
<div class="dc-row-2">63</div>
<div class="dc-row-3">858552</div>
<div class="dc-row-3">696730</div>
<div class="dc-row-4">307124</div>
<div class="dc-row-5">58414</div>
<div class="dc-row-6">209</div>
<div class="dc-row-7">808044</div>
</div>
<div class="data-column">
<div class="dc-row-1">331025</div>
<div class="dc-row-1">62118</div>
<div class="dc-row-2">2700</div>
<div class="dc-row-3">395852</div>
<div class="dc-row-3">604206</div>
<div class="dc-row-4">26</div>
<div class="dc-row-5">309150</div>
<div class="dc-row-6">885</div>
<div class="dc-row-7">210411</div>
</div>
<div class="data-column">
<div class="dc-row-1">817660</div>
<div class="dc-row-1">121979</div>
<div class="dc-row-2">20019</div>
<div class="dc-row-3">462869</div>
<div class="dc-row-3">25002</div>
<div class="dc-row-4">308</div>
<div class="dc-row-5">52074</div>
<div class="dc-row-6">33</div>
<div class="dc-row-7">80544</div>
</div>
<div class="data-column">
<div class="dc-row-1">1070</div>
<div class="dc-row-1">020478</div>
<div class="dc-row-2">26419</div>
<div class="dc-row-3">372122</div>
<div class="dc-row-3">2623</div>
<div class="dc-row-4">79</div>
<div class="dc-row-5">90008</div>
<div class="dc-row-6">8049</div>
<div class="dc-row-7">251664</div>
</div>
<div class="data-column">
<div class="dc-row-1">900007</div>
<div class="dc-row-1">704044</div>
<div class="dc-row-2">982365</div>
<div class="dc-row-3">258819</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">656214</div>
<div class="dc-row-5">409</div>
<div class="dc-row-6">218563</div>
<div class="dc-row-7">527222</div>
</div>
<div class="data-column">
<div class="dc-row-1">80106</div>
<div class="dc-row-1">1314577</div>
<div class="dc-row-2">39001</div>
<div class="dc-row-3">7162893</div>
<div class="dc-row-3">12855</div>
<div class="dc-row-4">57</div>
<div class="dc-row-5">23966</div>
<div class="dc-row-6">4</div>
<div class="dc-row-7">6244009</div>
</div>
<div class="data-column">
<div class="dc-row-1">2352</div>
<div class="dc-row-1">308</div>
<div class="dc-row-2">928</div>
<div class="dc-row-3">2721</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">402</div>
<div class="dc-row-5">540</div>
<div class="dc-row-6">795</div>
<div class="dc-row-7">23</div>
</div>
<div class="data-column">
<div class="dc-row-1">66880</div>
<div class="dc-row-1">8675309</div>
<div class="dc-row-2">821533</div>
<div class="dc-row-3">249009</div>
<div class="dc-row-3">51922</div>
<div class="dc-row-4">600454</div>
<div class="dc-row-5">9035768</div>
<div class="dc-row-6">453571</div>
<div class="dc-row-7">825064</div>
</div>
<div class="data-column">
<div class="dc-row-1">131488</div>
<div class="dc-row-1">641212</div>
<div class="dc-row-2">218035</div>
<div class="dc-row-3">37</div>
<div class="dc-row-3">6022</div>
<div class="dc-row-4">82</div>
<div class="dc-row-5">572104</div>
<div class="dc-row-6">799324</div>
<div class="dc-row-7">4404</div>
</div>
<div class="data-column">
<div class="dc-row-1">8807</div>
<div class="dc-row-1">4481</div>
<div class="dc-row-2">8915</div>
<div class="dc-row-3">2104</div>
<div class="dc-row-3">0000</div>
<div class="dc-row-4">326</div>
<div class="dc-row-5">446</div>
<div class="dc-row-6">8337</div>
<div class="dc-row-7">526</div>
</div>
<div class="data-column">
<div class="dc-row-1">593</div>
<div class="dc-row-1">8057</div>
<div class="dc-row-2">22</div>
<div class="dc-row-3">23</div>
<div class="dc-row-3">6722</div>
<div class="dc-row-4">890</div>
<div class="dc-row-5">2608</div>
<div class="dc-row-6">7274</div>
<div class="dc-row-7">2103</div>
</div>
</div>
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '#')">01</button>
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
</nav>
</div>
<div class="floor-text">
optical data network available
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<div class="bar-5"></div>
</div>
</div>
</div>
<div class="wrap" id="gap">
<div class="left-frame">
<!--
** SCROLL TO TOP OF PAGE BUTTON **
This button is styled like a panel in the sidebar and appears at the bottom of the page after scrolling down. If you don't want the sound effect, replace with this:
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
-->
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span class="hop">screen</span> top</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<!-- <button onclick="playSoundAndRedirect('audio1', '#')" class="sidebar-button">1-042</button>
<div class="sidebar-nav">
<a href="">1-042</a>
<a href="">2-079</a>
</div> -->
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
<div class="panel-7">07<span class="hop">-081940</span></div>
<div class="panel-8">08<span class="hop">-47148</span></div>
<div class="panel-9">09<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-10">10<span class="hop">-31</span></div>
</div>
</div>
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
<div class="bar-10"></div>
</div>
<main>
<!-- Start your content here. -->
<h1>Hello</h1>
<h2>Welcome to LCARS &#149; Nemesis Blue Theme &#149; Ultra Layout</h2>
<h3 class="font-lawn">Version 24.2</h3>
<h4>Replace This Content With Your Own</h4>
<p class="go-big">Live long and prosper.</p>
<!-- End content area. -->
</main>
<footer>
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
Content &copy; 2025 *replace this text with your website's name or URL.* <br>
<!-- The following attribution must not be removed: -->
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
</footer>
</div>
</div>
</section>
</div>
<script type="text/javascript" src="assets/lcars.js"></script>
<div class="headtrim"> </div>
<div class="baseboard"> </div>
</body>
</html>

82
templates/_base.html Normal file
View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="format-detection" content="date=no" />
<title>LAN Web</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='theme/LCARS/assets/classic.css') }}"
/>
{% block styles %}{% endblock %}
</head>
<body>
<audio
id="audio1"
src="{{ url_for('static', filename='theme/LCARS/assets/beep1.mp3') }}"
preload="auto"
></audio>
<audio
id="audio2"
src="{{ url_for('static', filename='theme/LCARS/assets/beep2.mp3') }}"
preload="auto"
></audio>
<audio
id="audio3"
src="{{ url_for('static', filename='theme/LCARS/assets/beep3.mp3') }}"
preload="auto"
></audio>
<audio
id="audio4"
src="{{ url_for('static', filename='theme/LCARS/assets/beep4.mp3') }}"
preload="auto"
></audio>
<section class="wrap-standard" id="column-3">
<div class="wrap">
<div class="left-frame-top">
<button
onclick="playSoundAndRedirect('audio1', '/')"
class="panel-1-button"
>
LAN Web
</button>
<div class="panel-2">Infrastructure Overview</div>
</div>
<div class="right-frame-top">
<div class="banner">
LAN Web Interface | zwitschi.net / allucanget.biz
</div>
{% include "_top.html" %}
</div>
</div>
<div class="wrap" id="gap">
{% include "_left.html" %}
<div class="right-frame">
<div class="bar-panel">
<div class="bar-6"></div>
<div class="bar-7"></div>
<div class="bar-8"></div>
<div class="bar-9"></div>
<div class="bar-10"></div>
</div>
<main>{% block content %}{% endblock %}</main>
<footer>
&copy; 2025 - LAN Web Interface for
<a href="https://allucanget.biz">allucanget.biz</a> by
<a href="https://zwitschi.net">zwitschi.net</a>
</footer>
</div>
</div>
</section>
<script src="{{ url_for('static', filename='theme/LCARS/assets/lcars.js') }}"></script>
<div class="headtrim"></div>
<div class="baseboard"></div>
</body>
</html>

23
templates/_left.html Normal file
View File

@@ -0,0 +1,23 @@
{% block left %}
<div class="left-frame">
<button
onclick="topFunction(); playSoundAndRedirect('audio4', '#')"
id="topBtn"
>
<span class="hop">screen</span> top
</button>
<div>
<div class="panel-3">03<span class="hop">-111968</span></div>
<div class="panel-4">04<span class="hop">-041969</span></div>
<div class="panel-5">05<span class="hop">-1701D</span></div>
<div class="panel-6">06<span class="hop">-071984</span></div>
<div class="panel-7">07<span class="hop">-081940</span></div>
<div class="panel-8">08<span class="hop">-47148</span></div>
<div class="panel-9">09<span class="hop">-081966</span></div>
</div>
<div>
<div class="panel-10">10<span class="hop">-31</span></div>
</div>
</div>
{% endblock %}

23
templates/_nav.html Normal file
View File

@@ -0,0 +1,23 @@
{% block nav %}
<nav>
<!--
*** MAIN NAVIGATION BUTTONS ***
Replace the hashtag '#' with a real URL (or not).
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
<a href="#">01</a>
<a href="#">02</a>
<a href="#">03</a>
<a href="#">04</a>
-->
<button onclick="playSoundAndRedirect('audio2', '/')">Systems</button>
<button onclick="playSoundAndRedirect('audio2', '/numbers')">Numbers</button>
<button onclick="playSoundAndRedirect('audio2', '/host/pve')">PVE</button>
<button onclick="playSoundAndRedirect('audio2', '/host/naspve')">
NASPVE
</button>
<!--
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
-->
</nav>
{% endblock %}

44
templates/_top.html Normal file
View File

@@ -0,0 +1,44 @@
{% block top %}
<script>
function getNumbers() {
fetch("/numbers")
.then((response) => response.json())
.then((data) => {
const wrapper = document.getElementById("default");
wrapper.innerHTML = ""; // Clear existing content
const lines = 24;
const columns = 9;
let index = 0;
for (let i = 0; i < columns; i++) {
const columnDiv = document.createElement("div");
columnDiv.className = "data-column";
for (let j = 0; j < lines; j++) {
if (index < data.length) {
const rowDiv = document.createElement("div");
rowDiv.className = `dc-row-${j + 1}`;
rowDiv.textContent = data[index];
columnDiv.appendChild(rowDiv);
index++;
}
}
wrapper.appendChild(columnDiv);
}
})
.catch((error) => console.error("Error fetching numbers:", error));
}
document.addEventListener("DOMContentLoaded", getNumbers);
</script>
<div class="data-cascade-button-group">
<div class="data-cascade-wrapper" id="default"></div>
{% include "_nav.html" %}
</div>
<div class="bar-panel first-bar-panel">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<div class="bar-5"></div>
</div>
{% endblock %}

6
templates/error.html Normal file
View File

@@ -0,0 +1,6 @@
{% extends '_base.html' %} {% block content %}
<h2>Error</h2>
<div class="lcars-frame">
<pre>{{ error }}</pre>
</div>
{% endblock %}

View File

@@ -0,0 +1,88 @@
{% extends '_base.html' %} {% block styles %}
<style>
#services {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.service {
font-size: 1.2em;
display: flex;
flex-direction: column;
}
.service-link {
display: flex;
width: var(--lfw);
justify-content: flex-end;
align-items: flex-end;
text-align: right;
vertical-align: bottom;
background-color: var(--panel-1-color);
min-height: clamp(60px, 10vw, 120px);
overflow: hidden;
padding: var(--left-frame-padding);
border-radius: 0;
border-bottom: var(--panel-border);
text-decoration: none;
color: black;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.if {
background-color: var(--orange);
}
.fs {
background-color: var(--blue);
}
.cmk {
background-color: var(--green);
}
.pve {
background-color: var(--lilac);
}
</style>
{% endblock %} {% block content %}
<h2>Services for {{ hostname }}</h2>
<div id="services">
{% for svc in services %}
<div class="service">
{% if svc.links %} {% for l in svc.links %} {% set classString =
'service-link' %} {% if 'Interface' in svc.extensions.description %} {% set
classString = classString + ' if' %} {% elif 'Filesystem' in
svc.extensions.description %} {% set classString = classString + ' fs' %} {%
elif 'Check_MK' in svc.extensions.description %} {% set classString =
classString + ' cmk' %} {% elif 'PVE' in svc.extensions.description %} {%
set classString = classString + ' pve' %} {% endif %}
<button class="{{ classString }}" data-link="{{ l.href }}">
{{ svc.extensions.description }}
</button>
{% endfor %} {% else %}
<span class="service-label"
>{{ svc.service_description or svc.title or svc.description or svc.name
}}</span
>
{% endif %}
</div>
{% endfor %}
</div>
<script>
function playSound(audioId) {
var audio = document.getElementById(audioId);
audio.play();
}
function serviceClick(event) {
event.preventDefault();
playSound("audio1");
let url = event.currentTarget.getAttribute("data-link");
let target = "/service/" + encodeURIComponent(url);
window.location.href = target;
}
document.querySelectorAll(".service-link").forEach(function (el) {
el.addEventListener("click", serviceClick);
});
</script>
{% endblock %}

63
templates/index.html Normal file
View File

@@ -0,0 +1,63 @@
{% extends '_base.html' %} {% block styles %}
<style>
.host {
font-size: 1.2em;
border: 1px solid var(--border-color);
padding: 10px;
margin-right: 10px;
border-radius: 8px;
background-color: var(--background-color);
min-width: 280px;
}
.host h3 a {
color: var(--blue);
}
.status-indicator {
margin-bottom: 10px;
}
.status-indicator.online {
color: var(--green);
}
.status-indicator.offline {
color: var(--red);
}
.checkmk-status {
margin-top: 10px;
font-weight: bold;
}
.checkmk-status.online {
color: var(--green);
}
.checkmk-status.offline {
color: var(--red);
}
</style>
{% endblock %} {% block content %}
<h2>Hosts</h2>
<div class="lcars-frame">
{% for h in hosts %}
<div class="host">
<h3>
<button
class="panel-1-button"
onclick="playSoundAndRedirect('audio1', '/host/{{ h.name }}')"
>
{{ h.name }}
</button>
</h3>
<div class="status-indicator {{ h.status }}">Status: {{ h.status }}</div>
CPU: {{ h.cpu }}<br />
Memory: {{ h.memory }}GB<br />
VMs: {{ h.vm_count }}<br />
Containers: {{ h.lxc_count }}<br />
<div
class="checkmk-status {% if h.check_mk.get('extensions') and h.check_mk.get('extensions').is_offline %}offline{% else %}online{% endif %}"
>
Monitoring: {% if h.check_mk.get('extensions') and
h.check_mk.get('extensions').is_offline %}OFFLINE{% else %}ONLINE{% endif
%}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends '_base.html' %} {% block styles %}
<style>
h1 {
margin-bottom: 20px;
}
#details {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.service-detail,
.service-status {
display: flex;
width: var(--lfw);
justify-content: flex-end;
align-items: flex-end;
text-align: right;
vertical-align: bottom;
background-color: var(--panel-1-color);
min-height: clamp(60px, 10vw, 120px);
overflow: hidden;
padding: var(--left-frame-padding);
border-radius: 0;
border-bottom: var(--panel-border);
text-decoration: none;
color: black;
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.service-status {
font-size: 1.5em;
font-weight: bold;
}
</style>
{% endblock %} {% block content %}
<h1>
Host {{ service.extensions.host_name }} - {{ service.extensions.description }}
Details
</h1>
<div id="details">
<div class="service-status">Status: {{ service.extensions.state }}</div>
<div class="service-detail">
Last Check: {{ service.extensions.last_check }}
</div>
</div>
<script>
function getServiceStateText(state) {
switch (state) {
case 0:
return "OK";
case 1:
return "WARNING";
case 2:
return "CRITICAL";
case 3:
return "UNKNOWN";
default:
return "N/A";
}
}
function getServiceStateClass(state) {
switch (state) {
case 0:
return "service-ok";
case 1:
return "service-warning";
case 2:
return "service-critical";
case 3:
return "service-unknown";
default:
return "service-na";
}
}
function getServiceStateColor(state) {
switch (state) {
case 0:
return "var(--green)";
case 1:
return "var(--yellow)";
case 2:
return "var(--red)";
case 3:
return "var(--orange)";
default:
return "var(--gray)";
}
}
document.addEventListener("DOMContentLoaded", function () {
var statusDiv = document.querySelector(".service-status");
var state = {{ service.extensions.state }};
statusDiv.textContent = "Status: " + getServiceStateText(state);
statusDiv.classList.add(getServiceStateClass(state));
statusDiv.style.backgroundColor = getServiceStateColor(state);
});
let service = {
links: [
{
domainType: "link",
rel: "self",
href: "http://192.168.88.91/monitoring/check_mk/api/1.0/objects/service/pve-TCP%2520Connections",
method: "GET",
type: "application/json",
},
],
domainType: "service",
id: "pve-TCP Connections",
title: "Service TCP Connections",
members: {},
extensions: {
host_name: "pve",
description: "TCP Connections",
state: 0,
state_type: 1,
last_check: 1757963587,
},
};
</script>
{% endblock %}

78
tests/test_app.py Normal file
View File

@@ -0,0 +1,78 @@
from app import app
import json
import pytest
import requests
# Patch requests.Session used by clients before importing app to avoid network calls
class SilentDummySession:
def __init__(self):
self.headers = {}
self.auth = None
def post(self, *args, **kwargs):
class R:
def raise_for_status(self):
return None
def json(self):
return {'data': {'ticket': 'TICKET', 'CSRFPreventionToken': 'CSRF'}}
return R()
def get(self, *args, **kwargs):
class R:
def raise_for_status(self):
return None
def json(self):
return {'data': []}
return R()
def _patch_sessions(monkeypatch):
monkeypatch.setattr('utils.proxmox_client.requests.Session',
lambda: SilentDummySession())
monkeypatch.setattr(
'utils.check_mk_client.requests.Session', lambda: SilentDummySession())
# Replace requests.Session globally immediately so importing app won't trigger real requests
requests.Session = lambda: SilentDummySession()
@pytest.fixture
def client(monkeypatch):
_patch_sessions(monkeypatch)
# mock proxmox and checkmk clients inside app
class DummyProx:
def get_cluster(self):
return {'nodes': [{'name': 'node1', 'status': 'online', 'maxmem': 1024, 'maxcpu': 2, 'qemu': []}]}
class DummyCheck:
def get_host_status(self, name):
return {'name': name, 'state': 'ok'}
def get_host_services(self, name):
return [{'service_description': 'ping', 'state': 'ok', 'output': 'OK - PING'}]
monkeypatch.setattr('app.proxmox', DummyProx())
monkeypatch.setattr('app.checkmk', DummyCheck())
app.testing = True
with app.test_client() as c:
yield c
def test_index(client):
r = client.get('/')
assert r.status_code == 200
assert b'node1' in r.data
def test_host_detail(client):
r = client.get('/host/node1')
assert r.status_code == 200
assert b'ping' in r.data

View File

@@ -0,0 +1,93 @@
import requests
import pytest
from utils.check_mk_client import CheckMKClient
class DummyResponse:
def __init__(self, json_data=None, text='', status_code=200):
self._json = json_data
self.text = text
self.status_code = status_code
def json(self):
if self._json is None:
raise ValueError('Invalid JSON')
return self._json
def raise_for_status(self):
if self.status_code >= 400:
raise requests.HTTPError(f'Status {self.status_code}')
class DummySession:
def __init__(self, mapping=None):
self.headers = {}
self.auth = None
self._mapping = mapping or {}
self.last_get = None
self.last_post = None
def get(self, url, params=None, verify=True, timeout=None):
self.last_get = dict(url=url, params=params,
verify=verify, timeout=timeout)
# choose response based on path substring
for k, v in self._mapping.items():
if k in url:
# v expected to be (json_data, text)
json_data, text, status = v
return DummyResponse(json_data, text or '', status or 200)
return DummyResponse({'result': []}, '', 200)
def post(self, url, headers=None, json=None, verify=True, timeout=None):
self.last_post = dict(url=url, headers=headers,
json=json, verify=verify, timeout=timeout)
for k, v in self._mapping.items():
if k in url:
json_data, text, status = v
return DummyResponse(json_data, text or '', status or 200)
return DummyResponse({'result': []}, '', 200)
def test_basic_auth_and_verify_and_ca_bundle(monkeypatch):
mapping = {
'/api/1.0/objects/host/host2': ({'result': {'host_name': 'host2', 'name': 'host2'}}, '', 200),
}
def fake_session_ctor():
return DummySession(mapping)
monkeypatch.setattr('requests.Session', fake_session_ctor)
client = CheckMKClient('https://checkmk.local',
user='u', password='p', verify=True)
# basic auth should be set on session
assert client.session.auth == ('u', 'p')
# default verify True should be passed through
_ = client.get_host_status('host2')
assert client.session.last_get['verify'] is True
# now supply ca_bundle path and ensure it is used as verify value
client2 = CheckMKClient('https://checkmk.local', user='u', password='p',
verify=True, api_token=None, ca_bundle='path/to/ca.pem')
# monkeypatch the session instance used by client2
client2.session = DummySession(mapping)
_ = client2.get_host_status('host2')
assert client2.session.last_get['verify'] == 'path/to/ca.pem'
def test_get_returns_raw_when_invalid_json(monkeypatch):
mapping = {
'/api/1.0/objects/host/any': (None, 'non-json response', 200),
}
def fake_session_ctor():
return DummySession(mapping)
monkeypatch.setattr('requests.Session', fake_session_ctor)
client = CheckMKClient('https://checkmk.local', api_token='t')
result = client.get_host_status('any')
# since JSON is invalid the method should return {} (no matching hosts)
assert result == {}

View File

@@ -0,0 +1,84 @@
import pytest
from utils.proxmox_client import ProxmoxClient
from utils.check_mk_client import CheckMKClient
class DummyResponse:
def __init__(self, json_data=None, text=''):
self._json = json_data or {}
self.text = text
def raise_for_status(self):
return None
def json(self):
return self._json
class DummySession:
def __init__(self):
self.headers = {}
self.auth = None
self.called = {}
def post(self, url, data=None, verify=True, timeout=None):
# record verify
self.called['post'] = {'url': url, 'verify': verify, 'data': data}
return DummyResponse({'data': {'ticket': 'TICKET', 'CSRFPreventionToken': 'CSRF'}})
def get(self, url, params=None, verify=True, timeout=None):
self.called['get'] = {'url': url, 'verify': verify, 'params': params}
# Return cluster resource like structure for proxmox
if 'cluster/resources' in url:
return DummyResponse({'data': [{'type': 'node', 'node': 'node1', 'status': 'online', 'maxmem': 1024, 'maxcpu': 2}]})
if 'qemu' in url:
return DummyResponse({'data': []})
# Check_MK endpoints (accept singular host endpoint too)
if '/api/1.0/objects/hosts' in url or '/api/1.0/objects/host' in url:
return DummyResponse({'result': [{'host_name': 'node1', 'state': 'ok'}]})
if '/api/1.0/objects/services' in url:
return DummyResponse({'result': [{'service_description': 'ping', 'state': 'ok', 'output': 'OK'}]})
return DummyResponse()
def test_proxmox_verify_and_token(monkeypatch):
dummy = DummySession()
# ensure the client's requests.Session() returns our dummy session
monkeypatch.setattr('utils.proxmox_client.requests.Session', lambda: dummy)
client = ProxmoxClient('https://pve.example/api2/json',
api_token='user!token=secret', verify=False, ca_bundle=None)
cluster = client.get_cluster()
assert 'nodes' in cluster
# ensure token set as header
assert 'Authorization' in dummy.headers
assert dummy.headers['Authorization'].startswith('PVEAPIToken=')
# ensure GET used verify=False
assert dummy.called['get']['verify'] == False
def test_proxmox_login_verify(monkeypatch):
dummy = DummySession()
# monkeypatch requests.Session so login uses DummySession
monkeypatch.setattr('utils.proxmox_client.requests.Session', lambda: dummy)
client = ProxmoxClient('https://pve.example/api2/json',
user='root@pam', password='pw', verify=True)
# login is performed lazily on first request; call get_cluster to trigger it
_ = client.get_cluster()
# ensure post verify True recorded
assert dummy.called['post']['verify'] == True
def test_checkmk_verify_and_auth(monkeypatch):
dummy = DummySession()
monkeypatch.setattr(
'utils.check_mk_client.requests.Session', lambda: dummy)
client = CheckMKClient('https://cmk.example',
api_token='secrettoken', verify=False)
# call get_host_status which will call GET on DummySession
status = client.get_host_status('node1')
assert status.get('host_name') == 'node1'
# verify header set
assert 'Authorization' in dummy.headers
assert dummy.called['get']['verify'] == False

0
utils/__init__.py Normal file
View File

60
utils/cache.py Normal file
View File

@@ -0,0 +1,60 @@
import hashlib
import json
from pathlib import Path
from typing import Any
import os
import time
# TTL for cache entries in seconds (24 hours)
CACHE_TTL = 24 * 60 * 60
CACHE_DIR = Path(__file__).resolve().parents[1] / 'cache'
CACHE_DIR.mkdir(parents=True, exist_ok=True)
def _key_to_filename(key: str) -> Path:
h = hashlib.sha256(key.encode('utf-8')).hexdigest()
return CACHE_DIR / f'{h}.json'
def read_cache(key: str) -> Any:
# avoid returning cached values during pytest runs to keep tests deterministic
if os.environ.get('PYTEST_CURRENT_TEST'):
return None
path = _key_to_filename(key)
if not path.exists():
return None
try:
with path.open('r', encoding='utf-8') as f:
payload = json.load(f)
# payload expected to be {'created_at': <ts>, 'data': <actual>}
created = payload.get('created_at')
if created is None:
return payload.get('data', None)
# expire after TTL
if (time.time() - created) > CACHE_TTL:
try:
path.unlink()
except Exception:
pass
return None
return payload.get('data', None)
except Exception:
return None
def write_cache(key: str, data: Any) -> None:
# avoid writing cache during pytest runs to prevent test cross-talk
if os.environ.get('PYTEST_CURRENT_TEST'):
return
path = _key_to_filename(key)
tmp = path.with_suffix('.tmp')
try:
payload = {'created_at': time.time(), 'data': data}
with tmp.open('w', encoding='utf-8') as f:
json.dump(payload, f)
tmp.replace(path)
except Exception:
if tmp.exists():
tmp.unlink()

263
utils/check_mk_client.py Normal file
View File

@@ -0,0 +1,263 @@
import json
import requests
from typing import Dict, Any, List, Optional
from utils.cache import read_cache, write_cache
SITE_NAME = 'monitoring'
PROTO = 'http' # or 'https'
INSTANCES = {
"pve": "192.168.88.91",
"naspve": "192.168.88.92"
}
PATHS = {
"host_status": "domain-types/host_config/collections/all",
"host_status_single": "objects/host/{hostname}",
"host_services": "domain-types/service/collections/all"
}
def get_api_url(hostname: str) -> str:
return f"{PROTO}://{INSTANCES[hostname]}/{SITE_NAME}/check_mk/api/1.0"
def get_api_endpoint(hostname: str, type: str) -> str:
base = f"{PROTO}://{INSTANCES[hostname]}/{SITE_NAME}/check_mk/api/1.0"
url = PATHS.get(type, '')
return f"{base}/{url}"
class CheckMKClient:
def __init__(self, base_url: str, user: Optional[str] = None, password: Optional[str] = None, api_token: Optional[str] = None, verify: bool = True, ca_bundle: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.user = user
self.password = password
self.api_token = api_token
self.verify = verify
self.ca_bundle = ca_bundle or None
# Use API token if provided (Check_MK uses 'Authorization: <token>' or 'OMD-LOGIN' depending on setup)
if api_token:
self.session.headers.update({'Authorization': api_token})
elif user and password:
self.session.auth = (user, password)
def _get(self, path: Optional[str], params: Optional[Dict[str, Any]] = None, url: Optional[str] = None) -> Dict[str, Any]:
url = url or f"{self.base_url}/{path.lstrip('/')}"
# Try cache first
cache_key = json.dumps({'method': 'GET', 'url': url, 'params': params, 'verify': (
self.ca_bundle if self.ca_bundle else self.verify)}, sort_keys=True)
cached = read_cache(cache_key)
if cached is not None:
# if session records last_get (used by tests), try to populate it
try:
self.session.last_get = dict(url=url, params=params,
verify=(self.ca_bundle if self.ca_bundle else self.verify), timeout=10)
except Exception:
pass
return cached
resp = self.session.get(
url,
params=params,
verify=(self.ca_bundle if self.ca_bundle else self.verify),
timeout=10,
)
resp.raise_for_status()
# Try to parse JSON; some Check_MK endpoints (e.g., Livestatus) may return plain text
try:
data = resp.json()
except ValueError:
data = {'raw': resp.text}
# write cache
try:
write_cache(cache_key, data)
except Exception:
pass
return data
def get_host_status(self, hostname: str) -> Dict[str, Any]:
# Query the host_config collection endpoint to retrieve all hosts,
# cache the full collection, and return the matching host from the cache.
try:
# Use the collection endpoint and these query params (as requested)
url_path = PATHS.get("host_status", "")
params = {
'effective_attributes': 'false',
'include_links': 'false',
'fields': '!(links)',
'site': 'monitoring',
}
# Build the collection URL from the configured base_url
url = f"{self.base_url.rstrip('/')}/{url_path.lstrip('/')}"
cache_key = json.dumps({'method': 'GET', 'url': url, 'params': params, 'verify': (
self.ca_bundle if self.ca_bundle else self.verify)}, sort_keys=True)
# Try cached collection first
cached = read_cache(cache_key)
if cached is not None:
data = cached
else:
resp = self.session.get(
url,
params=params,
verify=(self.ca_bundle if self.ca_bundle else self.verify),
timeout=20,
)
resp.raise_for_status()
try:
data = resp.json()
except ValueError:
data = None
# write full collection to cache (best-effort)
try:
write_cache(cache_key, data)
except Exception:
pass
except Exception:
return {}
# Normalize the collection into an iterable list of host objects
hosts = []
if data is None:
data = {}
if isinstance(data, dict):
if 'result' in data:
res = data.get('result')
if isinstance(res, list):
hosts = res
elif isinstance(res, dict):
hosts = list(res.values())
elif isinstance(data.get('hosts'), list):
hosts = data.get('hosts')
else:
vals = [v for v in data.values() if isinstance(v, dict)
or isinstance(v, list)]
for v in vals:
if isinstance(v, list):
hosts.extend(v)
elif isinstance(data, list):
hosts = data
# Find host by common keys in the collection
for h in hosts:
if not isinstance(h, dict):
continue
for key in ('id', 'name', 'host_name'):
if key in h and h.get(key) == hostname:
return h
# If collection didn't yield a match, fall back to the host-specific endpoint
try:
params = {'columns': ['name', 'host_name', 'state'], '_pretty': 1}
from urllib.parse import quote
safe_name = quote(hostname, safe='')
data2 = self._get(
path=f'/api/1.0/objects/host/{safe_name}', params=params)
except Exception:
data2 = None
if isinstance(data2, dict):
if 'result' in data2:
res = data2.get('result')
if isinstance(res, list):
return res[0] if res else {}
if isinstance(res, dict):
return res
if any(k in data2 for k in ('name', 'host_name', 'state')):
return data2
return {}
def get_host_services(self, hostname: str) -> List[Dict[str, Any]]:
# Use the collection POST endpoint to query services by host
try:
headers = {"Content-Type": "application/json"}
payload = {
"sites": ["monitoring"],
"columns": ["host_name", "description", "state"],
"query": {"op": "=", "left": "host_name", "right": hostname},
"host_name": hostname,
}
verify = self.ca_bundle if self.ca_bundle else self.verify
# perform POST with JSON payload; build URL from configured base_url
url = get_api_endpoint(hostname, 'host_services')
cache_key = json.dumps(
{'method': 'POST', 'url': url, 'json': payload, 'verify': verify}, sort_keys=True)
cached = read_cache(cache_key)
if cached is not None:
# try to populate session.last_post for test introspection
try:
self.session.last_post = dict(
url=url, headers=headers, json=payload, verify=verify, timeout=20)
except Exception:
pass
# normalize cached response to a list of services (tests expect a list)
if isinstance(cached, dict):
for key in ('result', 'value', 'services'):
res = cached.get(key)
if isinstance(res, list):
return res
if isinstance(res, dict):
return list(res.values())
if isinstance(cached, list):
return cached
return []
resp = self.session.post(
url,
headers={"Content-Type": "application/json"},
json=payload,
verify=(self.ca_bundle if self.ca_bundle else self.verify),
timeout=20,
)
resp.raise_for_status()
try:
data = resp.json()
except ValueError:
data = None
return []
# write cache
try:
write_cache(cache_key, data)
except Exception:
pass
except Exception as e:
return []
# data usually contains 'result' with a list of services
if isinstance(data, dict):
for key in ('result', 'value', 'services'):
res = data.get(key)
if isinstance(res, list):
return res
if isinstance(res, dict):
return list(res.values())
if isinstance(data, list):
return data
return []
def get_service_detail(self, service_url: str) -> Dict[str, Any]:
# Use the provided service URL to get detailed information about a specific service
base_url = service_url.split('/api/1.0')[0]
path = service_url.split('/api/1.0')[-1]
try:
data = self._get(url=service_url, path=path)
except Exception:
data = {}
if isinstance(data, dict):
return data
if isinstance(data, list):
if data:
return data[0]
return {}
return {}

125
utils/proxmox_client.py Normal file
View File

@@ -0,0 +1,125 @@
import json
import requests
from typing import Dict, Any, Optional
from utils.cache import read_cache, write_cache
class ProxmoxClient:
def __init__(self, base_url: str, user: Optional[str] = None, password: Optional[str] = None, api_token: Optional[str] = None, verify: bool = True, ca_bundle: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.user = user
self.password = password
self.api_token = api_token
self.csrf_token = None
self.verify = verify
self.ca_bundle = ca_bundle or None
# configure auth: prefer API token (PVEAPIToken=userid!tokenid=secret)
if api_token:
# API token format expected by env: <userid>!<tokenid>=<secret>
# We'll provide it as a header 'Authorization: PVEAPIToken=<token>'
self.session.headers.update(
{'Authorization': f'PVEAPIToken={api_token}'})
# Do not login during __init__ to avoid network calls at import time.
self._logged_in = False
def _login(self) -> None:
url = f"{self.base_url}/access/ticket"
resp = self.session.post(
url,
data={'username': self.user, 'password': self.password},
verify=(self.ca_bundle if self.ca_bundle else self.verify),
timeout=10,
)
resp.raise_for_status()
data = resp.json().get('data', {})
ticket = data.get('ticket')
csrf = data.get('CSRFPreventionToken') or data.get('csrf_token')
if ticket:
# set cookie for subsequent requests. real Session has cookies with set();
# if the session object doesn't support cookies (e.g., dummy in tests),
# fall back to adding a Cookie header.
try:
self.session.cookies.set('PVEAuthCookie', ticket)
except Exception:
# fallback to header
existing = self.session.headers.get('Cookie', '')
cookie_val = f'PVEAuthCookie={ticket}'
if existing:
c = str(existing) + '; ' + cookie_val
self.session.headers['Cookie'] = c
else:
self.session.headers['Cookie'] = cookie_val
if csrf:
self.csrf_token = csrf
self.session.headers.update({'CSRFPreventionToken': csrf})
# mark logged in so future requests don't re-login
self._logged_in = True
def _ensure_logged_in(self) -> None:
if self.api_token or self._logged_in:
return
# if PVEAuthCookie already present in headers, treat as logged in
if 'PVEAuthCookie' in self.session.headers.get('Cookie', ''):
self._logged_in = True
return
if self.user and self.password:
self._login()
def _get(self, path: str) -> Dict[str, Any]:
url = f"{self.base_url}/{path.lstrip('/')}"
# ensure authentication is ready before GET
self._ensure_logged_in()
# try cache first
cache_key = json.dumps(
{'method': 'GET', 'url': url, 'params': None}, sort_keys=True)
cached = read_cache(cache_key)
if cached is not None:
return cached
resp = self.session.get(url, verify=(
self.ca_bundle if self.ca_bundle else self.verify), timeout=10)
resp.raise_for_status()
try:
data = resp.json()
except ValueError:
data = {'raw': resp.text}
# write cache (best-effort)
try:
write_cache(cache_key, data)
except Exception:
pass
return data
def get_cluster(self) -> Dict[str, Any]:
# /cluster/resources returns resources including nodes
data = self._get('/cluster/resources')
nodes = []
for item in data.get('data', []):
if item.get('type') == 'node':
node_name = item.get('node')
try:
qemu = self._get(f'/nodes/{node_name}/qemu')
vms = qemu.get('data', [])
except Exception:
vms = []
try:
lxc = self._get(f'/nodes/{node_name}/lxc')
containers = lxc.get('data', [])
except Exception:
containers = []
node = {
'name': node_name,
'status': item.get('status'),
'memory': item.get('maxmem'),
'cpu': item.get('maxcpu'),
'qemu': vms,
'lxc': containers,
}
nodes.append(node)
return {'nodes': nodes}