commit a2984e6edf16938d5750af9b49e75979ff967c53 Author: fishtank-dashboard Date: Tue Mar 17 10:30:01 2026 -0700 Add files via upload diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c7b642 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# Fishtank Monitor + +A local dashboard for fishtank.live that shows live camera feeds, polls, TTS messages, stock prices, and more — all in one place without needing the main site open. + +--- + +## What You Need + +- A Windows, Mac, or Linux computer +- A fishtank.live account +- Node.js installed (free, takes 2 minutes) + +--- + +## Step 1 — Install Node.js + +Node.js is a small program that lets your computer run the server that powers this dashboard. + +1. Go to **https://nodejs.org** +2. Click the big green **"LTS"** download button (the recommended version) +3. Run the installer and click through — all default options are fine +4. When it's done, you're ready + +--- + +## Step 2 — Set Up the Files + +You should have two files: + +- `fishtank-dashboard.html` +- `server.js` + +Put both files in the **same folder** anywhere on your computer (e.g. your Desktop or a folder called `fishtank`). + +--- + +## Step 3 — Start the Server + +**On Windows:** +1. Open the folder where you put the files +2. Click the address bar at the top of the folder window and type `cmd`, then press Enter +3. A black window will appear. Type the following and press Enter: + ``` + node server.js + ``` + +**On Mac:** +1. Open the Terminal app (search for it in Spotlight) +2. Drag your folder into the Terminal window — this sets the location +3. Type the following and press Enter: + ``` + node server.js + ``` + +You should see: +``` +✓ Dashboard → http://localhost:3000 +✓ API proxy → http://localhost:3000/api/v1/... +✓ Cam proxy → http://localhost:3000/cam//index.m3u8 +``` + +**Leave this window open** — closing it stops the dashboard. + +--- + +## Step 4 — Open the Dashboard + +Open your browser and go to: + +``` +http://localhost:3000 +``` + +The dashboard will load with camera feeds starting automatically. + +--- + +## Step 5 — Link Your Account (Optional) + +Linking your fishtank.live account unlocks polls, TTS messages, stock prices, and other live data. The cameras work without it. + +**To get your token:** +1. Go to **fishtank.live** and log in +2. Press **F12** to open Developer Tools +3. Go to **Storage** (Firefox) or **Application** (Chrome) tab +4. Click **Cookies** → `https://www.fishtank.live` +5. Find the cookie named `sb-wcsaaupukpdmqdjcgaoo-auth-token` +6. Copy the entire **Value** field (it starts with `%5B%22eyJ...`) + +**To link it:** +1. Click the **LINK API** button in the top-left of the dashboard +2. Paste the value you copied into the input field +3. Press Enter or click **CONNECT** +4. The dot turns green and shows **API LIVE** when connected + +Your token is saved automatically — you won't need to do this again unless you log out or clear your browser data. The dashboard also refreshes the token automatically so it stays connected. + +--- + +## Stopping the Server + +In the terminal window where the server is running, press: +``` +Ctrl + C +``` + +If you closed the terminal window and need to stop it: + +- **Windows:** Open Task Manager → find `node.exe` → End Task +- **Mac/Linux:** Open Terminal and run `pkill node` + +--- + +## Features + +### 📹 Camera Feeds +- **17 live camera feeds** from the fishtank house displayed as thumbnails +- **Director Mode** plays as the main featured feed by default +- **Click any thumbnail** to switch it to the featured view — Director Mode takes its place in the grid +- **Click the cell showing Director Mode** in the grid to return to Director Mode +- **Double-click** the main feed to go fullscreen. Press Escape or double-click again to exit +- **Cameraman** feed displayed in its own panel next to the stocks chart, same switching behaviour +- **THUMBS / LIVE toggle** — Thumbs mode refreshes a snapshot every 30 seconds (saves bandwidth). Live mode streams all cameras simultaneously + +### 📊 Polls +- Shows the current active poll with live vote bars +- Displays the winner of the last poll +- Narrative polls are labelled with a **NARRATIVE POLL** badge + +### 💬 TTS Messages +- Accumulates TTS messages as they play in the house +- Shows the sender, voice used, room, status, cost, and timestamp +- **Click any message** to jump to the camera for that room +- A green/red dot next to the panel title shows whether TTS is currently enabled on the site + +### 📈 Stocks +- Line chart showing all contestant stock prices over time +- Toggle between **1D**, **1H**, and **1W** views +- Each stock has its own colour — click a stock in the legend to hide/show it +- Percentage change shown next to each ticker with up/down arrows + +### 📷 Capture Tools (in the camera panel header) +- **📷 SNAP** — takes a full resolution PNG screenshot of the current featured camera +- **✂ CLIP** — saves the last ~60 seconds of buffered footage as a WebM file +- **⏺ REC / ⏹ STOP** — records forward from when you click. A live file size counter shows how large the recording is getting +- **3.9MB LIMIT toggle** — when enabled, recording auto-stops before hitting 4MB (useful for sites with upload limits) +- **🔇 CLIPS MUTED / 🔊 AUDIO toggle** — controls whether clips and recordings include audio + +### ⚙️ Other Controls +- **VOL slider** — controls the volume of the featured camera +- **Interval selector** (5s / 10s / 30s / 60s) — how often the API data refreshes +- **LOGOUT** — clears your stored token and disconnects from the API + +--- + +## Troubleshooting + +**Cameras not loading:** +Make sure the server is running (`node server.js`) and you're visiting `http://localhost:3000` — not opening the HTML file directly. + +**"LINK API" not working:** +Make sure you copied the full cookie value starting with `%5B%22eyJ`. Just the token alone (starting with `eyJ`) also works but won't enable auto-refresh. + +**Server won't start:** +Make sure Node.js is installed. Open a terminal and type `node --version` — if it prints a version number, Node is installed. If not, go back to Step 1. + +**Port already in use:** +Something else is using port 3000. Either stop that program or edit `server.js` and change `const PORT = 3000` to another number like `3001`, then visit `http://localhost:3001` instead. diff --git a/fishtank-dashboard.html b/fishtank-dashboard.html new file mode 100644 index 0000000..50beae8 --- /dev/null +++ b/fishtank-dashboard.html @@ -0,0 +1,2067 @@ + + + + + +FISHTANK // MONITOR + + + + + + +
+ +
+
+ NO TOKEN + + + + +
+
+ +
+ +
+
+ +
+ +
+
+
ACTIVE POLL
+
+
+
+
📊
Waiting for data...
+
+
+ + +
+
+
STOCKS · TODAY
+
+ + + +
+
+
+
+
+
+ +
+
+
+ + +
+
+ +
CAMERAMAN
+
+
+ + +
+
+
TTS MESSAGES
+
+
+
+
💬
Waiting for data...
+
+
+ + +
+
+
CAMERAS
+
+ + + + + + + + VOL + +
+
+
+
+
+ +
+ + + + + diff --git a/server.js b/server.js new file mode 100644 index 0000000..addece0 --- /dev/null +++ b/server.js @@ -0,0 +1,161 @@ +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const url = require('url'); + +const PORT = 3000; +const API_TARGET = 'api.fishtank.live'; +const CAM_TARGET = 'epyc.goran.jetzt'; + +function fetchRemote(hostname, targetPath, callback) { + const options = { + hostname, + port: 443, + path: targetPath, + method: 'GET', + headers: { + 'host': hostname, + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'accept': '*/*', + 'origin': 'https://www.fishtank.live', + 'referer': 'https://www.fishtank.live/', + }, + }; + + const req = https.request(options, (remoteRes) => { + const chunks = []; + remoteRes.on('data', chunk => chunks.push(chunk)); + remoteRes.on('end', () => callback(null, remoteRes, Buffer.concat(chunks))); + }); + req.on('error', err => callback(err)); + req.end(); +} + +function rewriteM3u8(body, basePath) { + // basePath = e.g. /dirc-5/ + return body.split('\n').map(line => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + // Rewrite URI= attributes inside tag lines e.g. URI="tracks-a1/index.fmp4.m3u8" + return trimmed.replace(/URI="([^"]+)"/g, (match, uri) => { + if (uri.startsWith('http')) return match; + return `URI="http://localhost:${PORT}/cam${basePath}${uri}"`; + }); + } + if (trimmed.startsWith('http')) return trimmed; + return `http://localhost:${PORT}/cam${basePath}${trimmed}`; + }).join('\n'); +} + +function proxyApi(req, res, targetPath) { + const options = { + hostname: API_TARGET, + port: 443, + path: targetPath, + method: req.method, + headers: { + ...req.headers, + host: API_TARGET, + origin: 'https://www.fishtank.live', + referer: 'https://www.fishtank.live/', + }, + }; + delete options.headers['accept-encoding']; + + const proxy = https.request(options, (apiRes) => { + res.writeHead(apiRes.statusCode, { + ...apiRes.headers, + 'access-control-allow-origin': '*', + 'access-control-allow-headers': 'Authorization, Content-Type', + }); + apiRes.pipe(res); + }); + proxy.on('error', err => { res.writeHead(502); res.end(err.message); }); + req.pipe(proxy); +} + +const server = http.createServer((req, res) => { + const parsed = url.parse(req.url); + + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET, POST, OPTIONS', + 'access-control-allow-headers': 'Authorization, Content-Type', + }); + res.end(); + return; + } + + // Serve dashboard + if (parsed.pathname === '/' || parsed.pathname === '/dashboard') { + const file = path.join(__dirname, 'fishtank-dashboard.html'); + fs.readFile(file, (err, data) => { + if (err) { res.writeHead(404); res.end('Dashboard not found'); return; } + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(data); + }); + return; + } + + // API proxy + if (parsed.pathname.startsWith('/api/')) { + proxyApi(req, res, parsed.pathname.replace('/api', '') + (parsed.search || '')); + return; + } + + // Camera proxy + if (parsed.pathname.startsWith('/cam/')) { + const targetPath = parsed.pathname.replace('/cam', '') + (parsed.search || ''); + const isM3u8 = targetPath.includes('.m3u8'); + + if (!isM3u8) { + // Raw segment — pipe directly + fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => { + if (err) { res.writeHead(502); res.end(err.message); return; } + res.writeHead(remoteRes.statusCode, { + ...remoteRes.headers, + 'access-control-allow-origin': '*', + }); + res.end(body); + }); + return; + } + + // M3u8 — fetch and rewrite relative URLs + fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => { + if (err) { res.writeHead(502); res.end(err.message); return; } + if (remoteRes.statusCode !== 200) { + res.writeHead(remoteRes.statusCode); + res.end(body); + return; + } + + // Base path = directory containing this m3u8 + // e.g. /dirc-5/tracks-v2/index.fmp4.m3u8 → /dirc-5/tracks-v2/ + const basePath = targetPath.substring(0, targetPath.lastIndexOf('/') + 1); + const rewritten = rewriteM3u8(body.toString('utf8'), basePath); + + console.log(`✓ m3u8 ${targetPath} (${rewritten.split('\n').length} lines)`); + + res.writeHead(200, { + 'content-type': 'application/vnd.apple.mpegurl', + 'access-control-allow-origin': '*', + 'cache-control': 'no-cache', + }); + res.end(rewritten); + }); + return; + } + + res.writeHead(404); + res.end('Not found'); +}); + +server.listen(PORT, () => { + console.log('✓ Dashboard → http://localhost:' + PORT); + console.log('✓ Test cam → http://localhost:' + PORT + '/cam/dirc-5/index.m3u8'); + console.log('\nPress Ctrl+C to stop'); +}); +// This line intentionally left blank