diff --git a/deploy/nomadro-live/README.md b/deploy/nomadro-live/README.md new file mode 100644 index 0000000..badd4df --- /dev/null +++ b/deploy/nomadro-live/README.md @@ -0,0 +1,26 @@ +# nomadro.live — WordPress + audience split + +Reference implementation cloned from `nomadro.com` WordPress/router. +**Does not modify** `/var/www/nomadro-wp` or the TikTok promo databases. + +| Audience | Destination | +| --- | --- | +| Chinese users (`nomadrolive_route=zh`) | Existing Next.js `:3015` | +| English users / default | WordPress `:8089` | +| Googlebot / AdsBot / crawlers | WordPress | +| `/api/*` | Existing FastAPI `:8015` (Caddy, never through the router) | + +## Layout on server + +``` +/var/www/nomadro-live-wp/ docker compose (127.0.0.1:8089) +/var/www/nomadro-live-router/ node router (127.0.0.1:4404) +``` + +## Deploy + +```bash +python deploy/nomadro-live/remote_provision.py +``` + +Admin: `hackrobot` / `Xiao4669805@` at `https://nomadro.live/wp-admin/` diff --git a/deploy/nomadro-live/caddy/nomadro.live.caddy b/deploy/nomadro-live/caddy/nomadro.live.caddy new file mode 100644 index 0000000..39d5f1a --- /dev/null +++ b/deploy/nomadro-live/caddy/nomadro.live.caddy @@ -0,0 +1,14 @@ +# TikTok Promo Next.js + FastAPI, plus English WordPress via audience router. +# /api stays on the existing backend. HTML is split: zh → :3015, en/bots → :8089. + +nomadro.live, www.nomadro.live { + encode gzip + + handle /api/* { + reverse_proxy 127.0.0.1:8015 + } + + handle { + reverse_proxy 127.0.0.1:4404 + } +} diff --git a/deploy/nomadro-live/remote_provision.py b/deploy/nomadro-live/remote_provision.py new file mode 100644 index 0000000..aea2ee1 --- /dev/null +++ b/deploy/nomadro-live/remote_provision.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Provision nomadro.live English WordPress + audience router (non-destructive). + +What it does: +1) Uploads a fresh WordPress compose stack to /var/www/nomadro-live-wp (on 127.0.0.1:8089). +2) Uploads a dedicated audience router to /var/www/nomadro-live-router (on 127.0.0.1:4404). +3) Updates /etc/caddy/sites/nomadro.live.caddy so: + - /api/* continues to go to the existing FastAPI backend (:8015) + - other pages: + zh -> existing Next.js frontend (:3015) + en/default -> new WordPress (:8089) +4) Boots the router via systemd and runs bootstrap-wp.sh to seed 20 English articles. +""" + +from __future__ import annotations + +import os +import secrets +import string +import sys +import time +from pathlib import Path + +import paramiko + +HOST = "107.173.30.245" +USER = "root" + +PASSWORD = os.environ.get("NOMADRO_SSH_PASS", "") + +REMOTE_WP_DIR = "/var/www/nomadro-live-wp" +REMOTE_ROUTER_DIR = "/var/www/nomadro-live-router" +REMOTE_SYSTEMD_SERVICE = "/etc/systemd/system/nomadro-live-router.service" +REMOTE_CADDY_SITE = "/etc/caddy/sites/nomadro.live.caddy" + +LOCAL_DIR = Path(__file__).resolve().parent + +WP_LOCAL_DIR = LOCAL_DIR / "wordpress" +ROUTER_LOCAL_DIR = LOCAL_DIR / "router" +SYSTEMD_LOCAL_FILE = ROUTER_LOCAL_DIR / "nomadro-live-router.service" +CADDY_LOCAL_FILE = LOCAL_DIR / "caddy" / "nomadro.live.caddy" + + +def gen_pass(n: int = 24) -> str: + alphabet = string.ascii_letters + string.digits + return "".join(secrets.choice(alphabet) for _ in range(n)) + + +def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> None: + print(f"\n$ {cmd}") + _stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout, get_pty=True) + out = stdout.read().decode(errors="replace") + err = stderr.read().decode(errors="replace") + + def safe_print(s: str, file=None) -> None: + text = s.encode("ascii", "backslashreplace").decode("ascii") + print(text, file=file) if file is not None else print(text) + + if out.strip(): + safe_print(out[-4000:]) + if err.strip(): + safe_print(err[-2000:], file=sys.stderr) + code = stdout.channel.recv_exit_status() + if code != 0: + raise RuntimeError(f"Command failed ({code}): {cmd}") + + +def sftp_mkdirs(sftp: paramiko.SFTPClient, remote: str) -> None: + parts = remote.strip("/").split("/") + cur = "" + for part in parts: + cur += "/" + part + try: + sftp.stat(cur) + except FileNotFoundError: + sftp.mkdir(cur) + + +def upload_tree(sftp: paramiko.SFTPClient, local: Path, remote: str) -> None: + sftp_mkdirs(sftp, remote) + text_ext = {".sh", ".mjs", ".js", ".json", ".yml", ".yaml", ".html", ".php", ".txt", ".md", ".caddy", ".example", ".service"} + for path in local.rglob("*"): + if path.name.startswith(".env") or path.suffix in {".pyc"}: + continue + rel = path.relative_to(local).as_posix() + target = f"{remote}/{rel}" + if path.is_dir(): + sftp_mkdirs(sftp, target) + else: + sftp_mkdirs(sftp, str(Path(target).parent).replace("\\", "/")) + print(f"put {rel}") + if path.suffix.lower() in text_ext or path.name in {"Caddyfile", "Dockerfile"}: + data = path.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n") + sftp.putfo(__import__("io").BytesIO(data), target) + else: + sftp.put(str(path), target) + + +def wait_wp_http(ssh: paramiko.SSHClient, port: int, tries: int = 60) -> None: + for _ in range(tries): + _stdin, stdout, _stderr = ssh.exec_command( + f"curl -fsS -o /dev/null http://127.0.0.1:{port} || true", + timeout=20, + get_pty=True, + ) + ok = stdout.channel.recv_exit_status() == 0 + if ok: + print("WP HTTP ready on", f"127.0.0.1:{port}") + return + time.sleep(2) + raise RuntimeError(f"WordPress did not become ready on :{port}") + + +def main() -> int: + if not PASSWORD: + print("Set NOMADRO_SSH_PASS env var", file=sys.stderr) + return 1 + + if not LOCAL_DIR.exists(): + raise RuntimeError("Local deploy/nomadro-live folder missing") + + mysql_pass = gen_pass() + mysql_root = gen_pass() + + # User requested defaults + wp_admin_user = "hackrobot" + wp_admin_pass = "Xiao4669805@" + wp_admin_email = "hello@nomadro.live" + + env_text = "\n".join( + [ + f"MYSQL_PASSWORD={mysql_pass}", + f"MYSQL_ROOT_PASSWORD={mysql_root}", + f"WP_ADMIN_USER={wp_admin_user}", + f"WP_ADMIN_PASSWORD={wp_admin_pass}", + f"WP_ADMIN_EMAIL={wp_admin_email}", + "", + ] + ) + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(HOST, username=USER, password=PASSWORD, timeout=30, allow_agent=False, look_for_keys=False) + sftp = ssh.open_sftp() + + # Do not rotate DB passwords if the stack already exists. + env_exists = False + try: + sftp.stat(f"{REMOTE_WP_DIR}/.env") + env_exists = True + except FileNotFoundError: + env_exists = False + + print("==> Upload nomadro.live WordPress stack") + upload_tree(sftp, WP_LOCAL_DIR, REMOTE_WP_DIR) + if not env_exists: + with sftp.file(f"{REMOTE_WP_DIR}/.env", "w") as f: + f.write(env_text) + else: + print("Keeping existing remote .env (DB already initialized)") + sftp.chmod(f"{REMOTE_WP_DIR}/bootstrap-wp.sh", 0o755) + + # 2) Upload router code + print("\n==> Upload nomadro.live router") + upload_tree(sftp, ROUTER_LOCAL_DIR, REMOTE_ROUTER_DIR) + + # 3) Configure Caddy site (overwrite just this one file) + print("\n==> Update Caddy site for nomadro.live") + sftp.put(str(CADDY_LOCAL_FILE), REMOTE_CADDY_SITE) + sftp.chmod(REMOTE_CADDY_SITE, 0o644) + + # 4) Configure systemd service + print("\n==> Install systemd service for router") + sftp.put(str(SYSTEMD_LOCAL_FILE), REMOTE_SYSTEMD_SERVICE) + sftp.chmod(REMOTE_SYSTEMD_SERVICE, 0o644) + + sftp.close() + + # 5) Start docker stack + print("\n==> Start WordPress stack (docker compose up -d)") + run(ssh, f"cd {REMOTE_WP_DIR} && docker compose up -d", timeout=600) + + # Wait WP + wait_wp_http(ssh, 8089, tries=60) + + # 6) Seed content + trust pages + print("\n==> Bootstrap WordPress content (20 English posts)") + run(ssh, f"cd {REMOTE_WP_DIR} && bash bootstrap-wp.sh", timeout=900) + + # 7) Install router deps + start service (keep Caddy unchanged until router is ready) + print("\n==> Start nomadro-live-router") + run(ssh, f"cd {REMOTE_ROUTER_DIR} && npm install --omit=dev", timeout=600) + run(ssh, "systemctl daemon-reload", timeout=60) + # Restart is safe: service does not modify other apps/data. + run( + ssh, + "systemctl enable --now nomadro-live-router.service || systemctl restart nomadro-live-router.service", + timeout=60, + ) + + # 8) Validate Caddy and reload + print("\n==> Reload Caddy (activate nomadro.live split)") + run(ssh, "caddy validate --config /etc/caddy/Caddyfile", timeout=60) + run(ssh, "systemctl reload caddy", timeout=60) + + ssh.close() + print("\n=== nomadro.live provisioning done ===") + print(f"WP admin user: {wp_admin_user}") + print(f"WP admin pass: {wp_admin_pass}") + print(f"WP admin URL: https://nomadro.live/wp-admin/") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/deploy/nomadro-live/router/nomadro-live-router.service b/deploy/nomadro-live/router/nomadro-live-router.service new file mode 100644 index 0000000..896a5b3 --- /dev/null +++ b/deploy/nomadro-live/router/nomadro-live-router.service @@ -0,0 +1,17 @@ +[Unit] +Description=nomadro.live audience router (zh→promo Next, en→WordPress) +After=network.target tiktok-promo-frontend.service +Wants=tiktok-promo-frontend.service + +[Service] +Type=simple +WorkingDirectory=/var/www/nomadro-live-router +Environment=PORT=4404 +Environment=NEXT_UPSTREAM=http://127.0.0.1:3015 +Environment=WP_UPSTREAM=http://127.0.0.1:8089 +ExecStart=/usr/bin/node /var/www/nomadro-live-router/server.mjs +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/nomadro-live/router/package.json b/deploy/nomadro-live/router/package.json new file mode 100644 index 0000000..b12be33 --- /dev/null +++ b/deploy/nomadro-live/router/package.json @@ -0,0 +1,8 @@ +{ + "name": "nomadro-live-router", + "private": true, + "type": "module", + "dependencies": { + "http-proxy": "^1.18.1" + } +} diff --git a/deploy/nomadro-live/router/server.mjs b/deploy/nomadro-live/router/server.mjs new file mode 100644 index 0000000..aff18a6 --- /dev/null +++ b/deploy/nomadro-live/router/server.mjs @@ -0,0 +1,157 @@ +/** + * Audience router for nomadro.live + * - Chinese users → TikTok promo Next.js :3015 + * - English users / bots / default → WordPress :8089 + * + * /api/* is handled by Caddy (FastAPI :8015) and never reaches this process. + */ +import http from "node:http"; +import httpProxy from "http-proxy"; + +const PORT = Number(process.env.PORT || 4404); +const NEXT = process.env.NEXT_UPSTREAM || "http://127.0.0.1:3015"; +const WP = process.env.WP_UPSTREAM || "http://127.0.0.1:8089"; +const COOKIE = "nomadrolive_route"; +const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; + +const proxy = httpProxy.createProxyServer({ + xfwd: true, + changeOrigin: false, +}); + +proxy.on("error", (err, _req, res) => { + console.error("proxy error", err.message); + if (res && !res.headersSent && typeof res.writeHead === "function") { + res.writeHead(502, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Bad gateway"); + } +}); + +const BOT_RE = + /Googlebot|AdsBot-Google|Mediapartners-Google|Google-InspectionTool|bingbot|BingPreview|DuckDuckBot|Baiduspider|YandexBot|Slurp|facebookexternalhit|Twitterbot|LinkedInBot|Applebot|SemrushBot|AhrefsBot|DotBot|PetalBot|Bytespider/i; + +const WP_PATH_RE = + /^\/(wp-admin|wp-login\.php|wp-cron\.php|wp-comments-post\.php|xmlrpc\.php|wp-json|wp-content|wp-includes)(\/|$)/i; + +function getCookie(req, name) { + const raw = req.headers.cookie || ""; + for (const p of raw.split(/;\s*/)) { + const i = p.indexOf("="); + if (i === -1) continue; + if (p.slice(0, i) === name) return decodeURIComponent(p.slice(i + 1)); + } + return null; +} + +function isBot(req) { + return BOT_RE.test(req.headers["user-agent"] || ""); +} + +function wantsHtml(req) { + if (req.method !== "GET" && req.method !== "HEAD") return false; + const accept = req.headers.accept || ""; + const url = req.url || "/"; + const path = url.split("?")[0]; + if (path.startsWith("/_next/") || path.startsWith("/wp-")) return false; + if (/\.(js|css|map|png|jpe?g|gif|webp|svg|ico|woff2?|ttf|txt|xml)$/i.test(path)) { + return false; + } + if (!accept || accept === "*/*") return path === "/" || !path.includes("."); + return accept.includes("text/html"); +} + +function sniffHtml() { + return ` + + + + + + Nomadro Live + + + +

Loading…

+ + + +`; +} + +function resolveTarget(req) { + const url = req.url || "/"; + const path = url.split("?")[0]; + + if (WP_PATH_RE.test(path)) return WP; + if (isBot(req)) return WP; + if (url.includes("noscript=1")) return WP; + + const route = getCookie(req, COOKIE); + if (route === "zh") return NEXT; + if (route === "en") return WP; + + if (wantsHtml(req)) return "SNIFF"; + + const lang = String(req.headers["accept-language"] || "").toLowerCase(); + if (lang.includes("zh")) return NEXT; + return WP; +} + +const server = http.createServer((req, res) => { + const target = resolveTarget(req); + if (target === "SNIFF") { + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store, no-cache, must-revalidate", + }); + res.end(sniffHtml()); + return; + } + if (!req.headers["x-forwarded-proto"]) { + req.headers["x-forwarded-proto"] = "https"; + } + proxy.web(req, res, { target }); +}); + +server.on("upgrade", (req, socket, head) => { + let target = resolveTarget(req); + if (target === "SNIFF") target = WP; + proxy.ws(req, socket, head, { target }); +}); + +server.listen(PORT, "127.0.0.1", () => { + console.log(`nomadro-live-router 127.0.0.1:${PORT} zh→${NEXT} en→${WP}`); +}); diff --git a/deploy/nomadro-live/wordpress/.env.example b/deploy/nomadro-live/wordpress/.env.example new file mode 100644 index 0000000..17786f6 --- /dev/null +++ b/deploy/nomadro-live/wordpress/.env.example @@ -0,0 +1,5 @@ +MYSQL_PASSWORD=change-me +MYSQL_ROOT_PASSWORD=change-me +WP_ADMIN_USER=hackrobot +WP_ADMIN_PASSWORD=Xiao4669805@ +WP_ADMIN_EMAIL=hello@nomadro.live diff --git a/deploy/nomadro-live/wordpress/ads.txt b/deploy/nomadro-live/wordpress/ads.txt new file mode 100644 index 0000000..6d7968e --- /dev/null +++ b/deploy/nomadro-live/wordpress/ads.txt @@ -0,0 +1 @@ +google.com, pub-8461145959530971, DIRECT, f08c47fec0942fa0 diff --git a/deploy/nomadro-live/wordpress/bootstrap-wp.sh b/deploy/nomadro-live/wordpress/bootstrap-wp.sh new file mode 100644 index 0000000..03aa508 --- /dev/null +++ b/deploy/nomadro-live/wordpress/bootstrap-wp.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# Bootstrap English WordPress for nomadro.live (idempotent-ish). +set -eu +set -o pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$DIR" + +if [[ ! -f .env ]]; then + echo "Missing .env" + exit 1 +fi +# shellcheck disable=SC1091 +source .env + +WP() { + docker compose --profile tools run --rm wpcli "$@" +} + +echo "==> Waiting for WordPress HTTP..." +for i in $(seq 1 60); do + code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8089" || true) + if [[ "$code" =~ ^(200|301|302|403)$ ]]; then + break + fi + sleep 2 +done + +if ! WP core is-installed >/dev/null 2>&1; then + echo "==> Installing WordPress (English)" + WP core install \ + --url="https://nomadro.live" \ + --title="Nomadro Live" \ + --admin_user="${WP_ADMIN_USER}" \ + --admin_password="${WP_ADMIN_PASSWORD}" \ + --admin_email="${WP_ADMIN_EMAIL}" \ + --skip-email +fi + +echo "==> Core settings" +WP language core install en_US --activate || true +WP option update blogdescription "Guides for live-stream monitoring, creator operations, and event automation" +WP option update WPLANG "" +WP option update blog_public 1 +WP option update timezone_string "UTC" +WP option update date_format "F j, Y" +WP option update time_format "g:i a" +WP option update start_of_week 1 +WP option update show_on_front posts +WP rewrite structure '/%postname%/' --hard +WP rewrite flush --hard + +echo "==> Theme + essentials" +WP theme install twentytwentyfive --activate || WP theme activate twentytwentyfive || true +WP plugin install wordpress-seo --activate || true +WP option patch update wpseo_titles noindex-author-wpseo true || true + +HELLO_ID=$(WP post list --post_type=post --name=hello-world --field=ID 2>/dev/null || true) +if [[ -n "${HELLO_ID}" ]]; then + WP post delete "${HELLO_ID}" --force || true +fi +SAMPLE_ID=$(WP post list --post_type=page --name=sample-page --field=ID 2>/dev/null || true) +if [[ -n "${SAMPLE_ID}" ]]; then + WP post delete "${SAMPLE_ID}" --force || true +fi +# Default Privacy Policy page (often a draft) collides with our slug. +WP post list --post_type=page --post_status=any --format=ids | tr ' ' '\n' | while read -r pid; do + [ -z "$pid" ] && continue + name=$(WP post get "$pid" --field=post_name 2>/dev/null || true) + status=$(WP post get "$pid" --field=post_status 2>/dev/null || true) + if [[ "$name" == "privacy-policy" && "$status" != "publish" ]]; then + WP post delete "$pid" --force || true + fi +done + +create_page() { + local slug="$1" + local title="$2" + local file="$3" + local existing + existing=$(WP post list --post_type=page --name="$slug" --field=ID 2>/dev/null || true) + if [[ -n "$existing" ]]; then + echo "Page exists: $slug ($existing)" + return + fi + WP post create "$file" --post_type=page --post_title="$title" --post_name="$slug" --post_status=publish +} + +create_post() { + local slug="$1" + local title="$2" + local file="$3" + local existing + existing=$(WP post list --post_type=post --name="$slug" --field=ID 2>/dev/null || true) + if [[ -n "$existing" ]]; then + echo "Post exists: $slug ($existing)" + return + fi + WP post create "$file" --post_type=post --post_title="$title" --post_name="$slug" --post_status=publish +} + +echo "==> Policy / trust pages" +create_page about "About Nomadro Live" /content/about.html +create_page contact "Contact" /content/contact.html +create_page privacy-policy "Privacy Policy" /content/privacy-policy.html +create_page terms "Terms of Service" /content/terms.html +create_page disclaimer "Disclaimer" /content/disclaimer.html + +echo "==> English articles" +create_post what-is-a-live-monitoring-toolkit "What Is a Live Stream Monitoring Toolkit?" /content/posts/01-live-monitoring-toolkit.html +create_post how-to-track-live-gifts "How to Track Live Gifts Without Missing Events" /content/posts/02-gift-tracking.html +create_post multi-channel-live-monitoring "Multi-Channel Live Monitoring for Small Teams" /content/posts/03-multi-channel-ops.html +create_post webhook-automation-for-lives "Webhook Automation for Live Stream Events" /content/posts/04-webhook-automation.html +create_post desktop-vs-android-live-monitoring "Desktop vs Android for Live Monitoring" /content/posts/05-desktop-vs-android.html +create_post creator-ops-dashboard "Building a Creator Ops Dashboard That Gets Used" /content/posts/06-creator-ops-dashboard.html +create_post chat-moderation-workflow "Chat Moderation Workflows for Live Streams" /content/posts/07-chat-moderation-workflow.html +create_post reading-viewer-trend-charts "How to Read Viewer Trend Charts During a Live" /content/posts/08-viewer-trend-charts.html +create_post live-session-archives "Local Session Archives for Live Operations" /content/posts/09-session-archives.html +create_post overnight-live-coverage "Staffing Overnight Live Coverage Without Burnout" /content/posts/10-overnight-coverage.html +create_post notification-design-for-lives "Notification Design for High-Value Live Events" /content/posts/11-notification-design.html +create_post rest-api-live-event-tooling "REST API Basics for Live Event Tooling" /content/posts/12-rest-api-for-live-events.html +create_post reducing-false-alerts "Reducing False Alerts in Live Monitoring" /content/posts/13-false-alerts.html +create_post live-session-runbook "Preparing a One-Page Live Session Runbook" /content/posts/14-live-runbook.html +create_post creator-tooling-checklist "A Cross-Platform Creator Tooling Checklist" /content/posts/15-creator-tooling-checklist.html +create_post engagement-beyond-viewer-count "Measuring Engagement Beyond Viewer Count" /content/posts/16-engagement-beyond-viewers.html +create_post handling-live-disconnects "Handling Stream Disconnects and Reconnects" /content/posts/17-disconnects-and-reconnects.html +create_post privacy-for-live-logs "Privacy and Data Retention for Live Logs" /content/posts/18-privacy-live-logs.html +create_post hardware-for-always-on-monitoring "Choosing Hardware for Always-On Live Monitoring" /content/posts/19-hardware-for-24-7.html +create_post onboarding-live-operators "Onboarding a New Operator to Live Monitoring" /content/posts/20-onboarding-operators.html + +MENU_ID=$(WP menu list --fields=term_id,name --format=csv 2>/dev/null | awk -F, '$2=="Footer"{print $1; exit}' || true) +if [[ -z "${MENU_ID}" ]]; then + MENU_ID=$(WP menu create "Footer" --porcelain) +fi +for slug in about contact privacy-policy terms disclaimer; do + PAGE_ID=$(WP post list --post_type=page --name="$slug" --field=ID) + if [[ -n "$PAGE_ID" ]]; then + WP menu item add-post "$MENU_ID" "$PAGE_ID" || true + fi +done +WP menu location assign "$MENU_ID" footer || true +WP menu location assign "$MENU_ID" primary || true + +PRIVACY_ID=$(WP post list --post_type=page --name=privacy-policy --field=ID) +if [[ -n "${PRIVACY_ID}" ]]; then + WP option update wp_page_for_privacy_policy "${PRIVACY_ID}" || true +fi + +echo "==> Done. Admin: https://nomadro.live/wp-admin/" diff --git a/deploy/nomadro-live/wordpress/content/about.html b/deploy/nomadro-live/wordpress/content/about.html new file mode 100644 index 0000000..c99f913 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/about.html @@ -0,0 +1,11 @@ +

Nomadro Live is an English-language publication about live-stream operations: monitoring, alerts, session archives, and automation for creators and small teams.

+

The Chinese-language product site for the TikTok Live Monitor apps remains available to Chinese-speaking visitors. This English blog is the public editorial surface for guides, definitions, and operating checklists.

+

Editorial standards

+ +

Who publishes Nomadro Live

+

Content is produced by Nomadro Live Editorial. For corrections, partnerships, or questions, email hello@nomadro.live.

+

Nomadro Live is operated independently. We may display advertisements (including Google AdSense) to support the site.

diff --git a/deploy/nomadro-live/wordpress/content/contact.html b/deploy/nomadro-live/wordpress/content/contact.html new file mode 100644 index 0000000..854352c --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/contact.html @@ -0,0 +1,12 @@ +

We read every message. For the fastest reply, include your name, the page URL (if relevant), and a short summary.

+

Email

+

hello@nomadro.live

+

What to contact us about

+ +

Please do not send passwords, payment credentials, or other sensitive account details by email.

+

We aim to respond within a few business days.

diff --git a/deploy/nomadro-live/wordpress/content/disclaimer.html b/deploy/nomadro-live/wordpress/content/disclaimer.html new file mode 100644 index 0000000..4b3518d --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/disclaimer.html @@ -0,0 +1,10 @@ +

Last updated: August 24, 2026

+

Nomadro Live publishes educational guides about live-stream monitoring, creator operations, and related tooling.

+

General information only

+

Nothing on this website is personalized legal, tax, employment, financial, or professional advice. Platform rules, APIs, and creator policies change and vary by region.

+

Your responsibility

+

Before making decisions that affect your income, accounts, or compliance obligations, verify information with official sources and qualified professionals. This site is not affiliated with TikTok or ByteDance.

+

Affiliate and advertising disclosure

+

Nomadro Live may earn revenue from advertising partners, including Google AdSense, and may mention tools or services that could have commercial relationships in the future. Editorial recommendations aim to remain useful regardless of monetization.

+

Contact

+

Questions: hello@nomadro.live.

diff --git a/deploy/nomadro-live/wordpress/content/posts/01-live-monitoring-toolkit.html b/deploy/nomadro-live/wordpress/content/posts/01-live-monitoring-toolkit.html new file mode 100644 index 0000000..6599de7 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/01-live-monitoring-toolkit.html @@ -0,0 +1,16 @@ +

A live monitoring toolkit is software that captures stream events in real time so an operator does not have to stare at a single player window. It is not a replacement for the native live studio. It is an operations layer on top of it.

+

What it actually captures

+

Useful toolkits record chat, gifts, follows, shares, room joins, and viewer counts as discrete events with timestamps. The point is a structured feed you can filter, alert on, and archive. If a tool only screenshots a dashboard, you cannot reconstruct what happened after the fact.

+

Who needs one

+

Solo creators with long sessions, agencies running several rooms at once, and night-shift operators all hit the same limit: human attention. A toolkit exists to make attention scarce and events complete.

+

Local versus cloud

+

Local capture keeps session files on your machine. Cloud capture is easier to share across a team but introduces account, latency, and data-residency questions. Pick based on whether your primary risk is losing a laptop or leaking a chat log.

+

A minimum viable setup

+ +

What it is not

+

A monitoring toolkit does not grow your audience by itself. It does not bypass platform rules. Treat it as instrumentation for people who already run lives and need fewer missed events.

diff --git a/deploy/nomadro-live/wordpress/content/posts/02-gift-tracking.html b/deploy/nomadro-live/wordpress/content/posts/02-gift-tracking.html new file mode 100644 index 0000000..067df60 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/02-gift-tracking.html @@ -0,0 +1,11 @@ +

Gift events are dense, bursty, and easy to miss if you only watch an on-screen ticker. Tracking them as data lets you thank the right people, notice unusual spikes, and review a session without scrubbing a recording.

+

Capture the fields that matter

+

At minimum log sender handle, gift name, quantity, diamond or coin value if available, and timestamp. Without value, a “Rose x99” and a high-tier gift look the same in a naive count. Without a handle, you cannot follow up.

+

Burst handling

+

Combos arrive faster than a human can type. Debounce on-screen toasts so operators see a summary, but keep the raw events in the archive. Summaries are for the shift. Raw events are for the post-show recap.

+

High-value thresholds

+

Define two alert tiers before the show: “acknowledge in chat” and “pause other work and thank on camera.” Numbers differ by creator. The mistake is using one threshold for every room and then ignoring the alerts.

+

After the live

+

Export the gift table, sort by value, and mark who already received a public thank-you. Repeat gifters over several nights are a relationship list, not a one-off statistic.

+

Integrity

+

Do not invent gift values. If the upstream event omits a field, store it as unknown. Fake precision in recap decks trains teams to distrust the whole pipeline.

diff --git a/deploy/nomadro-live/wordpress/content/posts/03-multi-channel-ops.html b/deploy/nomadro-live/wordpress/content/posts/03-multi-channel-ops.html new file mode 100644 index 0000000..055bc46 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/03-multi-channel-ops.html @@ -0,0 +1,11 @@ +

Agencies that run several rooms at once fail first on attention allocation, not on software. Multi-channel monitoring only helps if you decide which rooms are primary and which are watch-only.

+

Primary versus secondary rooms

+

A primary room gets full chat response and gift acknowledgements. A secondary room gets alerts and a lighter check-in cadence. If every room is primary, none of them are.

+

Operator load

+

One operator can usually sustain one primary plus two secondary rooms if alerts are quiet. Four noisy primaries produce missed thanks and delayed moderation. Measure this with a two-hour trial, not a slide deck.

+

Layout

+

Put viewer trend and last high-value gift on a single glance row. Chat detail belongs in a selected-room panel, not eight simultaneous scrolling columns. Density that looks impressive in a demo is unusable at 2 a.m.

+

Handoffs

+

Write the room list, alert thresholds, and who is on camera into a shift note. Incoming operators should not reverse-engineer the setup from open windows.

+

Failure isolation

+

If one room’s connection dies, the other rooms must keep recording. Shared process crashes are the most expensive class of bug in this category. Prefer per-room isolation when you can.

diff --git a/deploy/nomadro-live/wordpress/content/posts/04-webhook-automation.html b/deploy/nomadro-live/wordpress/content/posts/04-webhook-automation.html new file mode 100644 index 0000000..7980d84 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/04-webhook-automation.html @@ -0,0 +1,11 @@ +

Webhooks turn live events into actions in other systems: a Discord ping, a spreadsheet row, a light on a desk, a CRM note. They are useful when the action is boring and time-sensitive.

+

Start with one rule

+

Pick a single event type—high-value gifts is the usual first choice—and send it to one destination you already check. Ten half-working rules are worse than one reliable rule.

+

Idempotency

+

Live feeds retry. Your receiver must tolerate duplicate event ids. If the upstream has no id, hash timestamp plus sender plus gift name and drop duplicates inside a short window.

+

Secrets and exposure

+

Webhook URLs are credentials. Do not paste them into public docs or client-side code. Rotate them when an operator leaves. Prefer signed payloads if the tool supports them.

+

Backpressure

+

A viral minute can fire hundreds of events. Queue on the receiver. Dropping events silently is better than crashing the live monitor process because a chat app rate-limited you.

+

Test before showtime

+

Send a synthetic event, confirm the destination, then disable the synthetic source. Debugging webhooks during a paid collab is a self-inflicted outage.

diff --git a/deploy/nomadro-live/wordpress/content/posts/05-desktop-vs-android.html b/deploy/nomadro-live/wordpress/content/posts/05-desktop-vs-android.html new file mode 100644 index 0000000..82cb6ed --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/05-desktop-vs-android.html @@ -0,0 +1,11 @@ +

Desktop and Android monitors solve the same event problem with different constraints. Choosing wrong usually means battery death, a blocked port, or a machine that cannot sit in a bag.

+

When desktop wins

+

Long sessions, multiple rooms, large charts, and webhook fan-out all prefer a plugged-in Windows machine. Keyboard shortcuts and a second monitor reduce misclicks during gifts.

+

When Android wins

+

A phone in a pocket can follow a single room while you are away from the desk. Foreground services and notification channels matter more than chart density. If the OS kills the process, you have no archive.

+

Battery and radios

+

Hotspot plus 24/7 capture plus screen-on is a thermal problem. Use a charger, disable unused radios, and test a four-hour run before you trust the device on a real show.

+

Notification design

+

Phone alerts should be reserved for thresholds you will actually act on. Desktop can show more toast noise because it is not in your pocket during dinner.

+

Shared operator habits

+

Use the same room list, the same gift thresholds, and the same archive naming on both platforms. Tool switching should not require a new mental model every night.

diff --git a/deploy/nomadro-live/wordpress/content/posts/06-creator-ops-dashboard.html b/deploy/nomadro-live/wordpress/content/posts/06-creator-ops-dashboard.html new file mode 100644 index 0000000..725a9c6 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/06-creator-ops-dashboard.html @@ -0,0 +1,16 @@ +

A creator ops dashboard is a decision surface, not a wallpaper of charts. If you cannot say what you will do when a number moves, remove the number.

+

Four tiles that earn their space

+ +

Baselines beat raw counts

+

2,000 viewers means nothing without the usual range for that creator and timeslot. Compute a rolling median from previous sessions and show deviation, not vanity totals.

+

Stale data is a bug

+

If events stop, the dashboard must look broken on purpose: a red last-seen timer beats a frozen pretty chart that implies the room is still live.

+

Role-specific views

+

The person on camera needs gifts and comments. The producer needs connection and queue. One layout for both roles produces ignored panels.

+

End of session

+

Lock the dashboard into a recap mode: peak viewers, top gifters, disconnect count. That recap is the only slide most clients will read.

diff --git a/deploy/nomadro-live/wordpress/content/posts/07-chat-moderation-workflow.html b/deploy/nomadro-live/wordpress/content/posts/07-chat-moderation-workflow.html new file mode 100644 index 0000000..7409905 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/07-chat-moderation-workflow.html @@ -0,0 +1,11 @@ +

Chat moderation during a live is a workflow, not a personality trait. Teams that skip the workflow either freeze on insults or over-block and kill the room.

+

Write the rules before the show

+

List banned topics, allowed teasing, and what happens on repeat offenses. If two moderators would disagree, the rule is not written yet.

+

Keyword alerts versus bans

+

Alerts are for review. Auto-hide is for unambiguous spam. Mixing them causes either alert fatigue or silent over-moderation. Keep the lists separate and short.

+

Escalation

+

First offense: hide or timeout. Second: longer timeout. Third: block and a one-line note in the session log. Skipping steps makes appeals impossible.

+

On-camera coordination

+

The host should not read every flagged line. Moderators send a three-word cue when the host must address something. Long Slack essays during a live are a failure mode.

+

Aftercare

+

Export flagged messages with timestamps. Review once, adjust the keyword list, and stop relitigating the same clip in group chat for three days.

diff --git a/deploy/nomadro-live/wordpress/content/posts/08-viewer-trend-charts.html b/deploy/nomadro-live/wordpress/content/posts/08-viewer-trend-charts.html new file mode 100644 index 0000000..8aa3998 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/08-viewer-trend-charts.html @@ -0,0 +1,11 @@ +

Viewer charts are useful when they change what you do in the next five minutes. They are decoration when they only confirm that a popular creator is popular.

+

Read slope, not peaks

+

A slow climb after a topic change is a stronger signal than a one-second spike from a raid that already ended. Train operators to talk about slope in the recap.

+

Align events to the curve

+

Overlay gifts, follows, and topic changes on the same timeline. A dip after a product pitch is information. A dip after a disconnect is a technical incident. Mixing the two in a client report is malpractice.

+

Sampling

+

If the source updates every few seconds, do not pretend you have millisecond precision. Store the native interval and label charts accordingly.

+

Compare like with like

+

Weekday lunch lives are not Saturday night lives. Compare against the same weekday and timeslot. Cross-slot comparisons create fake emergencies.

+

Action examples

+

Rising chat rate plus flat viewers: ask a question. Falling viewers plus gift burst: thank and hold the topic. Charts that never produce an action should leave the default screen.

diff --git a/deploy/nomadro-live/wordpress/content/posts/09-session-archives.html b/deploy/nomadro-live/wordpress/content/posts/09-session-archives.html new file mode 100644 index 0000000..750d6f9 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/09-session-archives.html @@ -0,0 +1,11 @@ +

Session archives are how teams learn. Without them, every live is a rumor. With them, you can answer who gifted, when chat spiked, and whether the disconnect was 20 seconds or 20 minutes.

+

What to store

+

Event log (JSON or CSV), a short metadata file (room, start, end, operator), and optionally a recap note. Video is optional and heavy. Start with events.

+

Naming

+

Use UTC timestamps in filenames: 2026-08-24T1900Z-room-handle. Human titles belong inside the metadata file. Clever folder art does not survive a second operator.

+

Retention

+

Decide a retention window before you accumulate a disk of chat logs. Personal data in comments is still personal data. Shorter is usually better.

+

Export formats

+

CSV for spreadsheets. JSON for tooling. If your only export is a screenshot, you do not have an archive.

+

Access control

+

Chat logs can include phone numbers, insults, and minors’ usernames. Do not dump archives into a shared drive with “everyone” access because it is convenient.

diff --git a/deploy/nomadro-live/wordpress/content/posts/10-overnight-coverage.html b/deploy/nomadro-live/wordpress/content/posts/10-overnight-coverage.html new file mode 100644 index 0000000..7807b85 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/10-overnight-coverage.html @@ -0,0 +1,11 @@ +

Overnight lives fail for predictable reasons: sleepy operators, silent alerts, and nobody who can restart a frozen client. Coverage is a staffing problem first.

+

Shifts, not heroics

+

Four-hour blocks with a 15-minute overlap beat a single person “watching until morning.” Overlap exists so the incoming person sees a live handoff, not a dead laptop.

+

Alert budget

+

Night alerts should be rarer and louder. If the phone buzzes for every follow, it will be in another room by 3 a.m. Keep night rules to disconnects and high-value gifts.

+

Physical setup

+

Charger, headset, written restart steps, and a backup hotspot. The overnight kit is boring on purpose. Novelty gadgets are for daytime experiments.

+

Health

+

Do not schedule the same person for consecutive overnights as a default. Error rates climb. So do accidental bans.

+

Morning recap

+

The day team needs: disconnect count, top gifts, unresolved moderation, and whether the host is still live. They do not need a novel.

diff --git a/deploy/nomadro-live/wordpress/content/posts/11-notification-design.html b/deploy/nomadro-live/wordpress/content/posts/11-notification-design.html new file mode 100644 index 0000000..acf01f7 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/11-notification-design.html @@ -0,0 +1,11 @@ +

Notifications are a scarce resource. Each unnecessary ping trains operators to ignore the next one, including the gift you actually needed.

+

Severity ladder

+

Info (log only), attention (desktop toast), interrupt (sound or phone). Most events belong in info. If everything is interrupt, you have no ladder.

+

Wording

+

Lead with the action: “Thank @user — Galaxy x1.” Do not lead with the product name of your monitoring app. The operator already knows which app buzzed.

+

Rate limits

+

Cap toasts per minute per room. Collapse repeats into “12 roses in 20s.” Humans cannot process a firehose and will blame the tool instead of the settings.

+

Quiet hours

+

Personal phones need a night profile even if the desktop stays loud. Split channels so family devices are not on the same stream as ops.

+

Review weekly

+

If an alert type was never acted on in seven days, demote it. Notification design is maintenance, not a one-time toggle screen.

diff --git a/deploy/nomadro-live/wordpress/content/posts/12-rest-api-for-live-events.html b/deploy/nomadro-live/wordpress/content/posts/12-rest-api-for-live-events.html new file mode 100644 index 0000000..78645a9 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/12-rest-api-for-live-events.html @@ -0,0 +1,11 @@ +

A local HTTP API plus an event stream lets other programs subscribe to a live without scraping a window. That is the difference between a toy overlay and a maintainable ops stack.

+

Read models

+

Expose current room state (viewers, uptime, last event time) as a cheap GET. Expose history as a filtered query, not an unbounded dump.

+

Event streams

+

Server-sent events are enough for most overlays. WebSockets are warranted when you need bidirectional control. Start with SSE unless you have a reason.

+

Auth

+

Bind to localhost by default. If you expose beyond the machine, require a token and TLS at the reverse proxy. “It is only on the LAN” is not an access policy.

+

Versioning

+

Prefix routes with /v1/. Live ops tools get forked by tired people at 1 a.m. Breaking JSON fields without a version is how overlays die mid-show.

+

Documentation

+

One page with example curl calls is more valuable than a generated OpenAPI that nobody opens. Keep the examples accurate when fields change.

diff --git a/deploy/nomadro-live/wordpress/content/posts/13-false-alerts.html b/deploy/nomadro-live/wordpress/content/posts/13-false-alerts.html new file mode 100644 index 0000000..b60ea72 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/13-false-alerts.html @@ -0,0 +1,11 @@ +

False alerts are not a minor annoyance. They are how on-call culture dies. A live monitor that cries wolf will be muted, and then a real disconnect goes unnoticed.

+

Common sources

+

Reconnect flaps, duplicated gift combos, timezone-shifted “new day” counters, and keyword lists that match ordinary slang. Log why an alert fired, not only that it fired.

+

Hysteresis

+

Require a condition to persist for N seconds before paging. Viewer drops of one second are noise. Disconnects that last 15 seconds are incidents.

+

Dedup windows

+

The same gifter sending the same gift three times in two seconds is usually a combo, not three incidents. Collapse it.

+

Operator feedback

+

Add a “not useful” control on toasts. Review the pile weekly. Settings that cannot be tuned will be abandoned.

+

Never hide failures

+

Suppressing flaps is good. Suppressing “no events for 90 seconds while the room should be live” is not. Distinguish noisy metrics from dead pipelines.

diff --git a/deploy/nomadro-live/wordpress/content/posts/14-live-runbook.html b/deploy/nomadro-live/wordpress/content/posts/14-live-runbook.html new file mode 100644 index 0000000..3c37b89 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/14-live-runbook.html @@ -0,0 +1,16 @@ +

A runbook is a short document you can follow when the host is talking and the producer is already busy. If it only lives in someone’s head, you do not have a runbook.

+

Pre-show (15 minutes)

+ +

During

+

Who thanks gifts, who mods chat, who restarts the client. Write names, not roles. Roles drift. Names do not.

+

Disconnect

+

Numbered steps: check host network, restart capture, verify events resume, post a status line to the team channel. No essays.

+

End of show

+

Stop capture, export archive, paste recap bullets, confirm the next shift or that the room is actually off.

+

Keep it one page

+

If the runbook needs a table of contents, it will not be read. Link out for deep docs. The page next to the keyboard stays thin.

diff --git a/deploy/nomadro-live/wordpress/content/posts/15-creator-tooling-checklist.html b/deploy/nomadro-live/wordpress/content/posts/15-creator-tooling-checklist.html new file mode 100644 index 0000000..bbe6d3a --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/15-creator-tooling-checklist.html @@ -0,0 +1,11 @@ +

Creator tooling sprawls until nobody can say what is load-bearing. A checklist keeps the stack small enough to restart under pressure.

+

Must-have

+

Stable capture of events, a way to notify a human, and an export. Everything else is optional until those three work on a bad Wi-Fi day.

+

Nice-to-have

+

Charts, multi-room, webhooks, mobile companion, widgets. Add one at a time and keep it for two weeks before adding another.

+

Account hygiene

+

Shared logins in a chat history are an incident waiting to happen. Use a password manager and revoke access when people leave the roster.

+

Platform policy

+

Automation that spam-clicks, mass-messages, or impersonates the official app is not “ops.” It is a ban path. Stay on documented or local-listen approaches your counsel can live with.

+

Review cadence

+

Once a month, delete a tool nobody opened. Tool counts only go up unless you schedule subtraction.

diff --git a/deploy/nomadro-live/wordpress/content/posts/16-engagement-beyond-viewers.html b/deploy/nomadro-live/wordpress/content/posts/16-engagement-beyond-viewers.html new file mode 100644 index 0000000..b09fafd --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/16-engagement-beyond-viewers.html @@ -0,0 +1,11 @@ +

Viewer count is the loudest metric and often the least actionable. Engagement is whether people talk, gift, follow, and stay after a topic change.

+

Chat rate

+

Messages per minute versus your baseline tells you if the room is awake. A high viewer count with dead chat is a lurking crowd, not a conversation.

+

Gift participation

+

Count unique gifters, not only diamond totals. One whale and a silent room is a different show from fifty small gifts.

+

Follow conversion

+

Follows clustered after a specific ask are evidence. Follows smeared across the whole session are harder to learn from. Align the ask with a timestamp in the archive.

+

Stay-through

+

If you can sample unique viewers over time, look at how many remain 10 minutes after a spike. Raids that bounce teach you not to change the content to chase them.

+

Pick two

+

Report two engagement metrics besides viewers. More than that and clients hear noise. Operators cannot optimize seven numbers at once.

diff --git a/deploy/nomadro-live/wordpress/content/posts/17-disconnects-and-reconnects.html b/deploy/nomadro-live/wordpress/content/posts/17-disconnects-and-reconnects.html new file mode 100644 index 0000000..ef4b391 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/17-disconnects-and-reconnects.html @@ -0,0 +1,11 @@ +

Disconnects are normal. Unhandled disconnects are how archives get holes and how hosts talk to an empty capture for twenty minutes.

+

Detect absence, not just errors

+

A client can sit in a “connected” UI state while events have stopped. Alert on last-event age, not only on thrown exceptions.

+

Reconnect policy

+

Exponential backoff with a cap. Hammering a session endpoint in a tight loop can make a platform issue worse and can look abusive.

+

User-visible status

+

Show “no events for 22s” in the operator UI. Hidden retries feel like a freeze. Visible retries feel like a system doing its job.

+

Host communication

+

If capture is down, the host may still be live on the official app. Do not tell them to end the stream unless you have confirmed the official player is also dark.

+

Post-incident

+

Log start, end, suspected cause (Wi-Fi, app crash, laptop sleep). Patterns across weeks beat a single angry screenshot.

diff --git a/deploy/nomadro-live/wordpress/content/posts/18-privacy-live-logs.html b/deploy/nomadro-live/wordpress/content/posts/18-privacy-live-logs.html new file mode 100644 index 0000000..50f882c --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/18-privacy-live-logs.html @@ -0,0 +1,11 @@ +

Live logs contain other people’s messages. Treating them like a private diary for the team is how you get a leak and a policy problem in the same week.

+

Minimize

+

Store what you need for ops and recap. You rarely need full message history for ninety days. You often need gift tables and a short moderation excerpt.

+

Access

+

Operators on the current roster only. Shared ZIP files on WeChat are not an access-control system.

+

Redaction

+

When sharing clips or CSVs with sponsors, strip phone numbers, emails, and obvious personal data. Default to handles, not real names.

+

Children

+

If a room attracts minors, tighten retention and sharing. “It was in a public chat” is not a complete answer to a parent or a platform.

+

Deletion

+

Have a way to delete a session file. Backups that ignore deletion requests are still copies of the data.

diff --git a/deploy/nomadro-live/wordpress/content/posts/19-hardware-for-24-7.html b/deploy/nomadro-live/wordpress/content/posts/19-hardware-for-24-7.html new file mode 100644 index 0000000..11fb9d5 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/19-hardware-for-24-7.html @@ -0,0 +1,11 @@ +

24/7 monitoring is a hardware reliability problem disguised as a software install. The weak points are power, heat, and sleep policies.

+

Power

+

Use a UPS if the building blinks. Disable Windows sleep on lid-close if the laptop must keep running closed—or use a small desktop so lids are irrelevant.

+

Network

+

Prefer ethernet. Keep a documented hotspot fallback. USB ethernet adapters are cheap compared with a missed collab.

+

Heat

+

A laptop on a bed running capture plus a browser plus a VPN will thermal-throttle and then throttle your attention. Elevate it. Dust the vents.

+

Displays

+

A second screen for chat versus charts reduces misclicks. It also increases power draw. Measure before you add a third.

+

Spares

+

One spare machine with the client already installed is worth more than a perfect primary with no backup. Image the spare quarterly.

diff --git a/deploy/nomadro-live/wordpress/content/posts/20-onboarding-operators.html b/deploy/nomadro-live/wordpress/content/posts/20-onboarding-operators.html new file mode 100644 index 0000000..ca8c857 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/posts/20-onboarding-operators.html @@ -0,0 +1,11 @@ +

New operators fail when training is a screen-share of vibes. Onboarding should be a sequence they can repeat without the senior person on the call.

+

Day one

+

Accounts, runbook location, where archives go, and a 20-minute shadow of a real room with no typing privileges. Watching is cheaper than a mistaken ban.

+

Day two

+

They handle alerts on a secondary room while a senior owns the primary. Debrief for fifteen minutes. Write down the two mistakes that actually happened.

+

Permissions

+

Grant the least access that still lets them work. Admin on day one is how credentials leak into screenshots.

+

Language and tone

+

If the host’s room has a specific voice, give three example replies—not a brand manifesto. Operators copy examples. They do not memorize essays.

+

Offboarding

+

Revoke tokens the same day someone leaves the roster. Rotate webhook URLs if they had them. Onboarding without offboarding is half a security program.

diff --git a/deploy/nomadro-live/wordpress/content/privacy-policy.html b/deploy/nomadro-live/wordpress/content/privacy-policy.html new file mode 100644 index 0000000..8739333 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/privacy-policy.html @@ -0,0 +1,35 @@ +

Last updated: August 24, 2026

+

Nomadro Live (“we”, “us”) operates https://nomadro.live. This Privacy Policy explains what information we collect, how we use it, and your choices. It is written to support transparency for readers and advertising partners such as Google AdSense.

+

Information we collect

+ +

How we use information

+ +

Advertising and Google AdSense

+

We use Google AdSense and related Google advertising services. Third-party vendors, including Google, use cookies to serve ads based on a user’s prior visits to this or other websites.

+

Google’s use of advertising cookies enables it and its partners to serve ads based on your visit to this site and/or other sites on the Internet. You may opt out of personalized advertising by visiting Google Ads Settings. You can also visit www.aboutads.info for more information about interest-based advertising opt-outs.

+

Analytics

+

We may use analytics tools to understand how visitors use the site. These tools may process IP addresses and usage events.

+

Data sharing

+

We do not sell personal information. We may share data with service providers that help us host, secure, analyze, or monetize the site, and when required by law.

+

Data retention

+

We retain information only as long as needed for the purposes described above, unless a longer period is required by law.

+

Your choices

+ +

Children

+

This site is not directed at children under 13. We do not knowingly collect personal information from children.

+

Changes

+

We may update this policy. The “Last updated” date at the top will change when we do.

+

Contact

+

Privacy questions: hello@nomadro.live.

diff --git a/deploy/nomadro-live/wordpress/content/terms.html b/deploy/nomadro-live/wordpress/content/terms.html new file mode 100644 index 0000000..06e9427 --- /dev/null +++ b/deploy/nomadro-live/wordpress/content/terms.html @@ -0,0 +1,20 @@ +

Last updated: August 24, 2026

+

Welcome to Nomadro Live. By accessing https://nomadro.live, you agree to these Terms of Service.

+

Use of the site

+

Content on Nomadro Live is for general informational and educational purposes. You agree not to misuse the site, attempt unauthorized access, scrape content at abusive scale, or interfere with normal operations.

+

No professional advice

+

Articles about live streaming, creator operations, tooling, data, or automation are not personalized legal, tax, employment, or professional advice. Always verify requirements for your own situation and consult qualified professionals when needed.

+

Intellectual property

+

Unless otherwise noted, site text, branding, and original media belong to Nomadro Live or its licensors. You may share links and short quotations with attribution. Do not republish full articles without permission.

+

Third-party links and services

+

We may link to third-party websites, tools, or partners. We are not responsible for their content, policies, or practices. Purchases or accounts you create with third parties are between you and those providers.

+

Advertising

+

The site may display advertisements, including through Google AdSense. Ad labeling and partner practices follow applicable partner policies.

+

Disclaimer of warranties

+

The site is provided “as is” and “as available.” We do not warrant uninterrupted availability, error-free content, or fitness for a particular purpose.

+

Limitation of liability

+

To the maximum extent permitted by law, Nomadro Live is not liable for indirect, incidental, consequential, or special damages arising from your use of the site.

+

Changes

+

We may update these terms. Continued use after changes means you accept the updated terms.

+

Contact

+

Questions: hello@nomadro.live.

diff --git a/deploy/nomadro-live/wordpress/docker-compose.yml b/deploy/nomadro-live/wordpress/docker-compose.yml new file mode 100644 index 0000000..c94beef --- /dev/null +++ b/deploy/nomadro-live/wordpress/docker-compose.yml @@ -0,0 +1,72 @@ +# nomadro.live English WordPress (AdSense) +# Binds to 127.0.0.1:8089 — do not collide with nomadro.com WP on 8088. + +name: nomadro-live-wp + +services: + db: + image: mariadb:11.4 + restart: unless-stopped + environment: + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 10 + + wordpress: + image: wordpress:6.8-php8.2-apache + restart: unless-stopped + depends_on: + db: + condition: service_healthy + ports: + - "127.0.0.1:8089:80" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} + WORDPRESS_DB_NAME: wordpress + WORDPRESS_CONFIG_EXTRA: | + define('WP_HOME', 'https://nomadro.live'); + define('WP_SITEURL', 'https://nomadro.live'); + if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $$_SERVER['HTTPS'] = 'on'; + } + define('FORCE_SSL_ADMIN', true); + volumes: + - wp_data:/var/www/html + - ./mu-plugins:/var/www/html/wp-content/mu-plugins:ro + + wpcli: + image: wordpress:cli-php8.2 + depends_on: + - wordpress + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} + WORDPRESS_DB_NAME: wordpress + WORDPRESS_CONFIG_EXTRA: | + define('WP_HOME', 'https://nomadro.live'); + define('WP_SITEURL', 'https://nomadro.live'); + if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $$_SERVER['HTTPS'] = 'on'; + } + volumes: + - wp_data:/var/www/html + - ./content:/content:ro + user: "33:33" + entrypoint: ["wp"] + command: ["--info"] + profiles: ["tools"] + +volumes: + db_data: + wp_data: diff --git a/deploy/nomadro-live/wordpress/mu-plugins/nomadro-live-adsense.php b/deploy/nomadro-live/wordpress/mu-plugins/nomadro-live-adsense.php new file mode 100644 index 0000000..d255ef7 --- /dev/null +++ b/deploy/nomadro-live/wordpress/mu-plugins/nomadro-live-adsense.php @@ -0,0 +1,112 @@ +' . "\n"; + echo '' . "\n"; + echo '' . "\n"; + echo '' . "\n"; + if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', NOMADRO_LIVE_UMAMI_WEBSITE_ID)) { + echo '' . "\n"; + } +}, 1); + +add_action('wp_head', static function (): void { + if (is_admin()) { + return; + } + $data = [ + '@context' => 'https://schema.org', + '@graph' => [ + [ + '@type' => 'Organization', + '@id' => home_url('/#organization'), + 'name' => 'Nomadro Live', + 'url' => home_url('/'), + 'email' => 'hello@nomadro.live', + 'description' => 'Practical guides for live-stream monitoring, creator operations, and event automation.', + ], + [ + '@type' => 'WebSite', + '@id' => home_url('/#website'), + 'url' => home_url('/'), + 'name' => 'Nomadro Live', + 'publisher' => ['@id' => home_url('/#organization')], + 'inLanguage' => 'en-US', + ], + ], + ]; + echo '' . "\n"; +}, 5); + +add_action('init', static function (): void { + $uri = isset($_SERVER['REQUEST_URI']) ? strtok($_SERVER['REQUEST_URI'], '?') : ''; + if ($uri === '/ads.txt') { + status_header(200); + header('Content-Type: text/plain; charset=utf-8'); + header('X-Robots-Tag: noindex'); + echo "google.com, pub-8461145959530971, DIRECT, f08c47fec0942fa0\n"; + exit; + } +}); + +add_action('wp_enqueue_scripts', static function (): void { + $css = <<<'CSS' +:root { --nomadro-accent:#7c3aed; } +body { -webkit-font-smoothing: antialiased; } +.entry-content, .wp-block-post-content { font-size: 1.0625rem; line-height: 1.75; } +.entry-content h2, .wp-block-post-content h2 { margin-top: 2rem; line-height: 1.3; } +.nomadro-trust-footer { + display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; + justify-content: center; padding: 1.5rem 1rem 2rem; font-size: 0.95rem; +} +.nomadro-trust-footer a { color: #344054; text-decoration: underline; } +CSS; + wp_register_style('nomadro-live-adsense-opt', false, [], '1.0.0'); + wp_enqueue_style('nomadro-live-adsense-opt'); + wp_add_inline_style('nomadro-live-adsense-opt', $css); +}, 20); + +add_action('wp_footer', static function (): void { + $links = [ + home_url('/about/') => 'About', + home_url('/contact/') => 'Contact', + home_url('/privacy-policy/') => 'Privacy Policy', + home_url('/terms/') => 'Terms of Service', + home_url('/disclaimer/') => 'Disclaimer', + ]; + echo ''; +}, 5); + +remove_action('wp_head', 'wp_generator'); +add_filter('the_generator', '__return_empty_string'); +add_filter('xmlrpc_enabled', '__return_false'); +add_filter('comments_open', '__return_false', 20, 2); +add_filter('pings_open', '__return_false', 20, 2); +add_filter('comments_array', static function () { + return []; +}, 10, 2); +add_filter('the_author', static function ($author) { + return 'Nomadro Live Editorial'; +}); +add_filter('get_the_author_display_name', static function () { + return 'Nomadro Live Editorial'; +});