diff --git a/.gitignore b/.gitignore
index 093c5a0..f1cf9ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ frigate_config.yml
# Generated configurations
nginx/nginx.conf
+nginx/index.html
# Vim temporary files
*~
diff --git a/nginx/index.html.template b/nginx/index.html.template
new file mode 100644
index 0000000..5fda501
--- /dev/null
+++ b/nginx/index.html.template
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Home IoT/SCADA Stack
+
+
+
+
+
+
+
+
+ Available Services
+
+
+ SERVICES_LIST
+
+
+
+
+
+
📚 Documentation
+
Access comprehensive documentation for setup and configuration
+
View Documentation
+
+
+
+
🔧 Features
+
+ MQTT Broker (Mosquitto)
+ Time Series Database (InfluxDB)
+ Data Visualization (Grafana)
+ Flow Automation (Node-RED)
+ NVR & Object Detection (Frigate)
+ Zigbee Gateway (Zigbee2MQTT)
+
+
+
+
+
🛡️ Security
+
Powered by Podman with automatic secret generation and hostname-based routing
+
Security Details
+
+
+
+
+
+
+
+
diff --git a/nginx/style.css b/nginx/style.css
new file mode 100644
index 0000000..361e5c2
--- /dev/null
+++ b/nginx/style.css
@@ -0,0 +1,277 @@
+/* Modern Dark Blueish Theme */
+:root {
+ --primary-bg: #0a1628;
+ --secondary-bg: #132337;
+ --card-bg: #1a2f4a;
+ --accent-blue: #2563eb;
+ --accent-teal: #06b6d4;
+ --text-primary: #e2e8f0;
+ --text-secondary: #94a3b8;
+ --border-color: #2d4663;
+ --hover-bg: #243b5a;
+ --success-green: #009639;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: linear-gradient(135deg, var(--primary-bg) 0%, #1a2c47 100%);
+ color: var(--text-primary);
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem 1.5rem;
+}
+
+/* Header Styles */
+header {
+ text-align: center;
+ margin-bottom: 3rem;
+ padding: 2rem 0;
+}
+
+.logo-section {
+ margin-bottom: 1.5rem;
+}
+
+.nginx-logo {
+ width: 120px;
+ height: auto;
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.3));
+}
+
+h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-teal) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 0.5rem;
+ letter-spacing: -0.5px;
+}
+
+.subtitle {
+ font-size: 1.125rem;
+ color: var(--text-secondary);
+ font-weight: 400;
+}
+
+/* Services Section */
+.services-section {
+ margin-bottom: 3rem;
+}
+
+.services-section h2 {
+ font-size: 1.75rem;
+ margin-bottom: 1.5rem;
+ color: var(--text-primary);
+ border-left: 4px solid var(--accent-blue);
+ padding-left: 1rem;
+}
+
+.services-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.25rem;
+ margin-bottom: 2rem;
+}
+
+.service-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 1.5rem;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ color: var(--text-primary);
+ display: block;
+}
+
+.service-card:hover {
+ background: var(--hover-bg);
+ border-color: var(--accent-blue);
+ transform: translateY(-4px);
+ box-shadow: 0 8px 16px rgba(37, 99, 235, 0.2);
+}
+
+.service-card h3 {
+ font-size: 1.25rem;
+ margin-bottom: 0.5rem;
+ color: var(--accent-teal);
+}
+
+.service-card p {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.75rem;
+}
+
+.service-card .service-url {
+ font-size: 0.875rem;
+ color: var(--accent-blue);
+ font-family: 'Courier New', monospace;
+}
+
+/* Info Section */
+.info-section {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+.info-card {
+ background: var(--secondary-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 2rem;
+ transition: all 0.3s ease;
+}
+
+.info-card:hover {
+ border-color: var(--accent-blue);
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
+}
+
+.info-card h3 {
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ color: var(--text-primary);
+}
+
+.info-card p {
+ color: var(--text-secondary);
+ margin-bottom: 1rem;
+ line-height: 1.7;
+}
+
+.features-list {
+ list-style: none;
+ padding-left: 0;
+}
+
+.features-list li {
+ padding: 0.5rem 0;
+ padding-left: 1.5rem;
+ color: var(--text-secondary);
+ position: relative;
+}
+
+.features-list li::before {
+ content: "▸";
+ position: absolute;
+ left: 0;
+ color: var(--accent-teal);
+ font-weight: bold;
+}
+
+.doc-link {
+ display: inline-block;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-teal) 100%);
+ color: white;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ margin-top: 0.5rem;
+}
+
+.doc-link:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(37, 99, 235, 0.4);
+}
+
+/* Footer */
+footer {
+ text-align: center;
+ padding: 2rem 0;
+ border-top: 1px solid var(--border-color);
+ margin-top: 3rem;
+ color: var(--text-secondary);
+}
+
+footer p {
+ margin: 0.5rem 0;
+}
+
+footer strong {
+ color: var(--success-green);
+ font-weight: 600;
+}
+
+.footer-note {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .container {
+ padding: 1.5rem 1rem;
+ }
+
+ h1 {
+ font-size: 2rem;
+ }
+
+ .subtitle {
+ font-size: 1rem;
+ }
+
+ .services-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .info-section {
+ grid-template-columns: 1fr;
+ }
+
+ .nginx-logo {
+ width: 100px;
+ }
+}
+
+@media (max-width: 480px) {
+ h1 {
+ font-size: 1.75rem;
+ }
+
+ .services-section h2 {
+ font-size: 1.5rem;
+ }
+
+ .info-card {
+ padding: 1.5rem;
+ }
+
+ .service-card {
+ padding: 1.25rem;
+ }
+}
+
+/* Loading Animation */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.service-card, .info-card {
+ animation: fadeIn 0.6s ease-out;
+}
diff --git a/startup.sh b/startup.sh
index c278136..b6499a0 100755
--- a/startup.sh
+++ b/startup.sh
@@ -104,14 +104,21 @@ http {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
- # Default server - redirect to available services
+ # Default server - serve landing page
server {
listen 80 default_server;
server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
location / {
- return 200 'Home IoT/SCADA Stack Home IoT/SCADA Stack ';
- add_header Content-Type text/html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location ~ \.(css|js|jpg|jpeg|png|gif|ico|svg)$ {
+ expires 30d;
+ add_header Cache-Control "public, immutable";
}
}
NGINX_EOF
@@ -226,20 +233,55 @@ NGINX_EOF
}
NGINX_EOF
- # Update the services list in the default page
+ # Update the services list in the landing page HTML
local services_html=""
if [ "$stack_type" == "iot_only" ] || [ "$stack_type" == "iot_nvr" ]; then
- services_html+="Grafana "
- services_html+="Node-RED "
- services_html+="Zigbee2MQTT "
+ services_html+=""
+ services_html+="Grafana "
+ services_html+="Data visualization and monitoring dashboards
"
+ services_html+="${GRAFANA_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
+
+ services_html+=""
+ services_html+="Node-RED "
+ services_html+="Flow-based automation and IoT integration
"
+ services_html+="${NODERED_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
+
+ services_html+=""
+ services_html+="Zigbee2MQTT "
+ services_html+="Zigbee device control and management
"
+ services_html+="${ZIGBEE2MQTT_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
fi
if [ "$stack_type" == "nvr_only" ] || [ "$stack_type" == "iot_nvr" ]; then
- services_html+="Frigate NVR "
- services_html+="Double-Take "
+ services_html+=""
+ services_html+="Frigate NVR "
+ services_html+="Network video recorder with object detection
"
+ services_html+="${FRIGATE_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
+
+ services_html+=""
+ services_html+="Double-Take "
+ services_html+="Facial recognition for Frigate
"
+ services_html+="${DOUBLETAKE_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
fi
- services_html+="openSUSE Cockpit "
+ services_html+=""
+ services_html+="openSUSE Cockpit "
+ services_html+="System management and monitoring console
"
+ services_html+="${COCKPIT_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
- sed -i "s|SERVICES_LIST|${services_html}|g" "${nginx_conf_file}"
+ # Copy template and update the HTML file with the services list
+ local html_template="./nginx/index.html.template"
+ local html_file="./nginx/index.html"
+ if [ ! -f "${html_template}" ]; then
+ echo "ERROR: HTML template file not found at ${html_template}"
+ return 1
+ fi
+ cp "${html_template}" "${html_file}"
+ sed -i "s|SERVICES_LIST|${services_html}|g" "${html_file}"
echo "Nginx configuration generated at ${nginx_conf_file}"
}
@@ -270,14 +312,21 @@ http {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
- # Default server - redirect to available services
+ # Default server - serve landing page
server {
listen 80 default_server;
server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
location / {
- return 200 'Home IoT/SCADA Stack Home IoT/SCADA Stack ';
- add_header Content-Type text/html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location ~ \.(css|js|jpg|jpeg|png|gif|ico|svg)$ {
+ expires 30d;
+ add_header Cache-Control "public, immutable";
}
}
NGINX_EOF
@@ -303,7 +352,11 @@ NGINX_EOF
}
}
NGINX_EOF
- services_html+="Grafana "
+ services_html+=""
+ services_html+="Grafana "
+ services_html+="Data visualization and monitoring dashboards
"
+ services_html+="${GRAFANA_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
else
echo " [INFO] Grafana is not running - skipping from nginx config"
fi
@@ -330,7 +383,11 @@ NGINX_EOF
}
}
NGINX_EOF
- services_html+="Node-RED "
+ services_html+=""
+ services_html+="Node-RED "
+ services_html+="Flow-based automation and IoT integration
"
+ services_html+="${NODERED_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
else
echo " [INFO] Node-RED is not running - skipping from nginx config"
fi
@@ -357,7 +414,11 @@ NGINX_EOF
}
}
NGINX_EOF
- services_html+="Zigbee2MQTT "
+ services_html+=""
+ services_html+="Zigbee2MQTT "
+ services_html+="Zigbee device control and management
"
+ services_html+="${ZIGBEE2MQTT_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
else
echo " [INFO] Zigbee2MQTT is not running - skipping from nginx config"
fi
@@ -381,7 +442,11 @@ NGINX_EOF
}
}
NGINX_EOF
- services_html+="Frigate NVR "
+ services_html+=""
+ services_html+="Frigate NVR "
+ services_html+="Network video recorder with object detection
"
+ services_html+="${FRIGATE_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
else
echo " [INFO] Frigate is not running - skipping from nginx config"
fi
@@ -405,7 +470,11 @@ NGINX_EOF
}
}
NGINX_EOF
- services_html+="Double-Take "
+ services_html+=""
+ services_html+="Double-Take "
+ services_html+="Facial recognition for Frigate
"
+ services_html+="${DOUBLETAKE_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
else
echo " [INFO] Double-Take is not running - skipping from nginx config"
fi
@@ -447,10 +516,21 @@ NGINX_EOF
}
NGINX_EOF
- services_html+="openSUSE Cockpit "
+ services_html+=""
+ services_html+="openSUSE Cockpit "
+ services_html+="System management and monitoring console
"
+ services_html+="${COCKPIT_HOSTNAME}.${BASE_DOMAIN} "
+ services_html+=" "
- # Update the services list in the default page
- sed -i "s|SERVICES_LIST|${services_html}|g" "${nginx_conf_file}"
+ # Copy template and update the HTML file with the services list
+ local html_template="./nginx/index.html.template"
+ local html_file="./nginx/index.html"
+ if [ ! -f "${html_template}" ]; then
+ echo "ERROR: HTML template file not found at ${html_template}"
+ return 1
+ fi
+ cp "${html_template}" "${html_file}"
+ sed -i "s|SERVICES_LIST|${services_html}|g" "${html_file}"
echo "Nginx configuration generated at ${nginx_conf_file} based on running services"
}
@@ -746,7 +826,7 @@ SERVICE_CMDS[zigbee2mqtt]="podman run -d --name zigbee2mqtt --restart unless-sto
SERVICE_CMDS[frigate]="podman run -d --name frigate --restart unless-stopped --network ${NETWORK_NAME} --privileged -e TZ=${TZ} -p ${FRIGATE_PORT}:5000/tcp -p 1935:1935 -v ${FRIGATE_RECORDINGS_HOST_PATH}:/media/frigate:rw -v ./frigate_config.yml:/config/config.yml:ro -v /etc/localtime:/etc/localtime:ro --shm-size 256m ghcr.io/blakeblackshear/frigate:stable"
SERVICE_CMDS[grafana]="podman run -d --name grafana --restart unless-stopped --network ${NETWORK_NAME} -p 3000:3000 -v grafana_data:/var/lib/grafana -e GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} -e GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} -e GF_SECURITY_SECRET_KEY=${GRAFANA_SECRET_KEY} docker.io/grafana/grafana:latest"
SERVICE_CMDS[nodered]="podman run -d --name nodered --restart unless-stopped --network ${NETWORK_NAME} -p ${NODERED_PORT}:1880 -e TZ=${TZ} -e DOCKER_HOST=unix:///var/run/docker.sock -v nodered_data:/data -v ${PODMAN_SOCKET_PATH}:/var/run/docker.sock:ro --security-opt label=disable --user root docker.io/nodered/node-red:latest"
-SERVICE_CMDS[nginx]="podman run -d --name nginx --restart unless-stopped --network ${NETWORK_NAME} --add-host=host.containers.internal:host-gateway -p 80:80 --security-opt label=disable -v ${PWD}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v nginx_cache:/var/cache/nginx docker.io/library/nginx:alpine"
+SERVICE_CMDS[nginx]="podman run -d --name nginx --restart unless-stopped --network ${NETWORK_NAME} --add-host=host.containers.internal:host-gateway -p 80:80 --security-opt label=disable -v ${PWD}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro -v ${PWD}/nginx/index.html:/usr/share/nginx/html/index.html:ro -v ${PWD}/nginx/style.css:/usr/share/nginx/html/style.css:ro -v nginx_cache:/var/cache/nginx docker.io/library/nginx:alpine"
SERVICE_CMDS[doubletake]="podman run -d --name doubletake --restart unless-stopped --network ${NETWORK_NAME} -p 3001:3000 -v doubletake_data:/.storage -e TZ=${TZ} docker.io/jakowenko/double-take:latest"
SERVICE_NAMES=(mosquitto influxdb zigbee2mqtt frigate grafana nodered nginx doubletake)