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 + + + +
+
+
+ +
+

Home IoT/SCADA Stack

+

Containerized IoT & Home Automation Platform

+
+ +
+
+

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)