Rebrand StreamHoard, strip Emergent.sh platform artifacts

- Remove .emergent/ state dir, tracked .gitconfig, @emergentbase/visual-edits
  dep + craco wiring, emergent.sh script/meta tags, PostHog telemetry snippet
- Rename UI/docs branding Kino -> StreamHoard (README, DEPLOY.md, Navbar,
  Login, Register, AdminShowEpisodes, FastAPI title, health check response)
- Rename docker-compose container/network names to streamhoard-*
- Default admin email domain -> admin@streamhoard.local
This commit is contained in:
Myron Blair
2026-07-23 20:20:29 -05:00
parent d4dffdb6fb
commit e27bb37f7b
18 changed files with 53 additions and 155 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"env_image_name": "fastapi_react_mongo_shadcn_base_image_cloud_arm:release-17042026-1",
"job_id": "08d9a7a1-2a0c-4502-9939-fc5d643c04c4",
"created_at": "2026-07-23T22:57:32.873830+00:00Z"
}
-3
View File
@@ -1,3 +0,0 @@
[user]
email = github@emergent.sh
name = emergent-agent-e1
-1
View File
@@ -61,7 +61,6 @@ logs/
# --- Test reports / agent runtime ---
test_reports/
.emergent/
tests/__pycache__/
# --- Misc ---
+11 -11
View File
@@ -1,8 +1,8 @@
# Kino — Deployment on a Proxmox LAMP host
# StreamHoard — Deployment on a Proxmox LAMP host
Kino is FastAPI + MongoDB + React. It runs happily alongside your existing
StreamHoard is FastAPI + MongoDB + React. It runs happily alongside your existing
LAMP stack (Apache/MySQL/PHP) because we run everything in Docker containers
on their own network. Apache keeps port 80/443, MySQL is untouched, Kino
on their own network. Apache keeps port 80/443, MySQL is untouched, StreamHoard
uses whatever port you pick.
---
@@ -54,7 +54,7 @@ Log in with the admin credentials from `.env`.
## Running behind your existing Apache (optional, recommended)
If you already have Apache on port 80/443 with virtual hosts, add a Kino vhost
If you already have Apache on port 80/443 with virtual hosts, add a StreamHoard vhost
that reverse-proxies to the container. This gives you `https://kino.example.com`
instead of `http://ip:8080`.
@@ -130,7 +130,7 @@ To reset everything: `docker compose down -v` (deletes the mongo-data volume).
## Coexistence with your LAMP stack
| Kino uses | LAMP uses | Conflict? |
| StreamHoard uses | LAMP uses | Conflict? |
| ------------------------ | ----------------------- | --------- |
| Port `KINO_HTTP_PORT` | Apache 80/443 | No — different ports |
| MongoDB (internal only) | MySQL | No — different DBs, Mongo not exposed to host |
@@ -144,16 +144,16 @@ same box.
## Radarr integration
If Radarr is running on the same Proxmox host (native or container), point Kino
If Radarr is running on the same Proxmox host (native or container), point StreamHoard
at it via **Admin → Settings → Radarr**:
- **URL**: `http://<host-ip>:7878` (use the host IP, not `localhost` — containers
can't reach the host's `localhost` unless you add `host.docker.internal`)
- **API key**: from Radarr → Settings → General → Security
**Important**: For Kino to actually play the imported movies, the path Radarr
**Important**: For StreamHoard to actually play the imported movies, the path Radarr
reports (e.g. `/movies/Inception (2010)/Inception.mkv`) must be **readable from
inside the Kino backend container**. Mount Radarr's media path into Kino:
inside the StreamHoard backend container**. Mount Radarr's media path into StreamHoard:
```yaml
# docker-compose.yml — extend the backend volumes
@@ -163,7 +163,7 @@ backend:
- /mnt/radarr-movies:/mnt/radarr-movies:ro # <— add this
```
Radarr's file paths must be identical inside both Radarr and Kino containers
Radarr's file paths must be identical inside both Radarr and StreamHoard containers
(easiest: match the paths exactly).
---
@@ -172,14 +172,14 @@ Radarr's file paths must be identical inside both Radarr and Kino containers
```bash
curl -s http://localhost:${KINO_HTTP_PORT:-8080}/api/ | jq
# {"app":"Kino","status":"ok"}
# {"app":"StreamHoard","status":"ok"}
```
---
## Firewalling
Kino has no built-in rate limiting or IP allowlist. Since you confirmed
StreamHoard has no built-in rate limiting or IP allowlist. Since you confirmed
**internal network only**, keep it that way:
```bash
+4 -4
View File
@@ -1,4 +1,4 @@
# Kino — Personal Media Server (Netflix Clone)
# StreamHoard — Personal Media Server (Netflix Clone)
A self-hosted Netflix-style streaming app for movies you legally own.
Built with FastAPI + React + MongoDB.
@@ -41,7 +41,7 @@ See **[DEPLOY.md](./DEPLOY.md)** for LAMP-coexistence, Apache reverse proxy, Rad
*(Legal alternative to auto-downloading — auto-download of copyrighted material is not implemented.)*
## Default admin
- Email: `admin@kino.local`
- Email: `admin@streamhoard.local`
- Password: `kino-admin-2026`
Change `ADMIN_EMAIL` / `ADMIN_PASSWORD` in `backend/.env` before first start
@@ -118,7 +118,7 @@ Movie media files should live on your Proxmox storage, not Git.
# Login
curl -X POST $URL/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@kino.local","password":"kino-admin-2026"}'
-d '{"email":"admin@streamhoard.local","password":"kino-admin-2026"}'
# List movies
curl $URL/api/movies
@@ -130,7 +130,7 @@ curl $URL/api/stream/<movie_id>?auth=<token>
## Why no auto-download?
Auto-downloading copyrighted movies from the internet is illegal in most
jurisdictions (DMCA, EU copyright directive, etc.). Kino is built as a
jurisdictions (DMCA, EU copyright directive, etc.). StreamHoard is built as a
**personal media server** for content you legally own or that is in the
public domain (Internet Archive, Blender Open Movies, etc.).
+3 -3
View File
@@ -57,7 +57,7 @@ for d in (VIDEOS_DIR, POSTERS_DIR, SUBS_DIR, HLS_DIR):
CHUNK_SIZE = 1024 * 1024
app = FastAPI(title="Kino")
app = FastAPI(title="StreamHoard")
api = APIRouter(prefix="/api")
bearer = HTTPBearer(auto_error=False)
@@ -114,7 +114,7 @@ async def on_startup():
await db.profiles.create_index("user_id")
await db.subtitles.create_index("movie_id")
admin_email = os.environ.get("ADMIN_EMAIL", "admin@kino.local")
admin_email = os.environ.get("ADMIN_EMAIL", "admin@streamhoard.local")
if not await db.users.find_one({"email": admin_email}):
admin_user = {
"id": str(uuid.uuid4()),
@@ -1482,7 +1482,7 @@ async def _auto_fetch_subtitle(content_type: str, content_id: str, tmdb_id: Opti
# ============ HEALTH ============
@api.get("/")
async def root():
return {"app": "Kino", "status": "ok"}
return {"app": "StreamHoard", "status": "ok"}
app.include_router(api)
+6 -6
View File
@@ -1,5 +1,5 @@
"""
Backend pytest suite for Kino Personal Media Server.
Backend pytest suite for StreamHoard Personal Media Server.
Tests auth, movies, watchlist, progress, requests, and stream auth.
"""
import os
@@ -8,8 +8,8 @@ import uuid
import pytest
import requests
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "https://streamhoard.preview.emergentagent.com").rstrip("/")
ADMIN_EMAIL = "admin@kino.local"
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "http://localhost:8001").rstrip("/")
ADMIN_EMAIL = "admin@streamhoard.local"
ADMIN_PASSWORD = "kino-admin-2026"
@@ -32,7 +32,7 @@ def admin_token(api):
@pytest.fixture(scope="session")
def member_token(api):
email = f"TEST_user_{uuid.uuid4().hex[:8]}@kino.local"
email = f"TEST_user_{uuid.uuid4().hex[:8]}@streamhoard.local"
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Test User"})
assert r.status_code == 200, f"register failed: {r.text}"
data = r.json()
@@ -73,7 +73,7 @@ class TestAuth:
assert r.status_code == 401
def test_register_and_me(self, api):
email = f"TEST_reg_{uuid.uuid4().hex[:8]}@kino.local"
email = f"TEST_reg_{uuid.uuid4().hex[:8]}@streamhoard.local"
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "Reg User"})
assert r.status_code == 200
d = r.json()
@@ -86,7 +86,7 @@ class TestAuth:
assert r2.json()["email"] == email.lower()
def test_register_duplicate(self, api):
email = f"TEST_dup_{uuid.uuid4().hex[:8]}@kino.local"
email = f"TEST_dup_{uuid.uuid4().hex[:8]}@streamhoard.local"
api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
r = api.post(f"{BASE_URL}/api/auth/register", json={"email": email, "password": "pass1234", "name": "U"})
assert r.status_code == 400
+3 -3
View File
@@ -9,7 +9,7 @@ import pytest
import requests
BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "").rstrip("/")
ADMIN_EMAIL = "admin@kino.local"
ADMIN_EMAIL = "admin@streamhoard.local"
ADMIN_PASSWORD = "kino-admin-2026"
@@ -36,7 +36,7 @@ def admin_headers(admin_token):
@pytest.fixture(scope="module")
def member(api):
"""Create a fresh member for profile-scoped tests."""
email = f"TEST_p2_{uuid.uuid4().hex[:8]}@kino.local"
email = f"TEST_p2_{uuid.uuid4().hex[:8]}@streamhoard.local"
r = api.post(f"{BASE_URL}/api/auth/register",
json={"email": email, "password": "pass1234", "name": "P2 User"})
assert r.status_code == 200
@@ -101,7 +101,7 @@ class TestProfiles:
def test_cannot_delete_last_profile(self, api):
# fresh user with single profile
email = f"TEST_solo_{uuid.uuid4().hex[:6]}@kino.local"
email = f"TEST_solo_{uuid.uuid4().hex[:6]}@streamhoard.local"
d = api.post(f"{BASE_URL}/api/auth/register",
json={"email": email, "password": "pass1234", "name": "Solo"}).json()
h = {"Authorization": f"Bearer {d['access_token']}"}
+2 -2
View File
@@ -17,7 +17,7 @@ import pytest
import requests
BASE_URL = os.environ["REACT_APP_BACKEND_URL"].rstrip("/")
ADMIN_EMAIL = "admin@kino.local"
ADMIN_EMAIL = "admin@streamhoard.local"
ADMIN_PASSWORD = "kino-admin-2026"
QTEST_VIDEO = "/tmp/qtest.mp4"
@@ -40,7 +40,7 @@ def admin_token(api):
@pytest.fixture(scope="session")
def member_token(api):
email = f"TEST_q_{uuid.uuid4().hex[:8]}@kino.local"
email = f"TEST_q_{uuid.uuid4().hex[:8]}@streamhoard.local"
r = api.post(f"{BASE_URL}/api/auth/register",
json={"email": email, "password": "pass1234", "name": "QTest"},
headers={"Content-Type": "application/json"})
+13 -13
View File
@@ -1,9 +1,9 @@
# Kino — Docker Compose deployment
# StreamHoard — Docker Compose deployment
#
# Runs alongside existing LAMP stacks without conflicts:
# - Apache keeps port 80/443
# - MySQL is untouched (Kino uses its own MongoDB in a container)
# - Kino exposes a single configurable port (default 8080)
# - MySQL is untouched (StreamHoard uses its own MongoDB in a container)
# - StreamHoard exposes a single configurable port (default 8080)
#
# Usage:
# cp .env.compose.example .env
@@ -14,19 +14,19 @@
services:
mongo:
image: mongo:4.4
container_name: kino-mongo
container_name: streamhoard-mongo
restart: unless-stopped
volumes:
- mongo-data:/data/db
networks:
- kino
# No ports exposed to host — only reachable inside kino network
- streamhoard
# No ports exposed to host — only reachable inside streamhoard network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: kino-backend
container_name: streamhoard-backend
restart: unless-stopped
depends_on:
- mongo
@@ -37,7 +37,7 @@ services:
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
JWT_ALG: HS256
JWT_EXPIRE_HOURS: ${JWT_EXPIRE_HOURS:-720}
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@kino.local}
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@streamhoard.local}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?set ADMIN_PASSWORD in .env}
ADMIN_NAME: ${ADMIN_NAME:-Admin}
MEDIA_ROOT: /media
@@ -45,25 +45,25 @@ services:
# Host media directory (movies, HLS, subtitles) — point at your NAS/ZFS mount
- ${MEDIA_ROOT_HOST:-./media}:/media
networks:
- kino
- streamhoard
# No direct host port — frontend container reverse-proxies /api → backend:8001
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: kino-frontend
container_name: streamhoard-frontend
restart: unless-stopped
depends_on:
- backend
ports:
# Only Kino's external port. Change KINO_HTTP_PORT to avoid clashes with Apache.
# Only StreamHoard's external port. Change KINO_HTTP_PORT to avoid clashes with Apache.
- "${KINO_HTTP_PORT:-8080}:80"
networks:
- kino
- streamhoard
networks:
kino:
streamhoard:
driver: bridge
volumes:
-20
View File
@@ -2,10 +2,6 @@
const path = require("path");
require("dotenv").config();
// Check if we're in development/preview mode (not production build)
// Craco sets NODE_ENV=development for start, NODE_ENV=production for build
const isDevServer = process.env.NODE_ENV !== "production";
// Environment variable overrides
const config = {
enableHealthCheck: process.env.ENABLE_HEALTH_CHECK === "true",
@@ -81,20 +77,4 @@ webpackConfig.devServer = (devServerConfig) => {
return devServerConfig;
};
// Wrap with visual edits (automatically adds babel plugin, dev server, and overlay in dev mode)
if (isDevServer) {
try {
const { withVisualEdits } = require("@emergentbase/visual-edits/craco");
webpackConfig = withVisualEdits(webpackConfig);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND' && err.message.includes('@emergentbase/visual-edits/craco')) {
console.warn(
"[visual-edits] @emergentbase/visual-edits not installed — visual editing disabled."
);
} else {
throw err;
}
}
}
module.exports = webpackConfig;
-1
View File
@@ -76,7 +76,6 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@craco/craco": "^7.1.0",
"@emergentbase/visual-edits": "https://assets.emergent.sh/npm/emergentbase-visual-edits-1.0.8.tgz",
"@eslint/js": "9.23.0",
"autoprefixer": "^10.4.20",
"eslint": "9.23.0",
+2 -74
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="A product of emergent.sh" />
<meta name="description" content="StreamHoard - self-hosted personal media server" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet" />
@@ -21,9 +21,8 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Emergent | Fullstack App</title>
<title>StreamHoard</title>
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
<script src="https://assets.emergent.sh/scripts/emergent-main.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -38,76 +37,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
!(function (t, e) {
var o, n, p, r;
e.__SV ||
((window.posthog = e),
(e._i = []),
(e.init = function (i, s, a) {
function g(t, e) {
var o = e.split(".");
2 == o.length && ((t = t[o[0]]), (e = o[1])),
(t[e] = function () {
t.push(
[e].concat(
Array.prototype.slice.call(
arguments,
0,
),
),
);
});
}
((p = t.createElement("script")).type =
"text/javascript"),
(p.crossOrigin = "anonymous"),
(p.async = !0),
(p.src =
s.api_host.replace(
".i.posthog.com",
"-assets.i.posthog.com",
) + "/static/array.js"),
(r =
t.getElementsByTagName(
"script",
)[0]).parentNode.insertBefore(p, r);
var u = e;
for (
void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
u.people = u.people || [],
u.toString = function (t) {
var e = "posthog";
return (
"posthog" !== a && (e += "." + a),
t || (e += " (stub)"),
e
);
},
u.people.toString = function () {
return u.toString(1) + ".people (stub)";
},
o =
"init me ws ys ps bs capture je Di ks register register_once register_for_session unregister unregister_for_session Ps getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Es $s createPersonProfile Is opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing Ss debug xs getPageViewId captureTraceFeedback captureTraceMetric".split(
" ",
),
n = 0;
n < o.length;
n++
)
g(u, o[n]);
e._i.push([i, s, a]);
}),
(e.__SV = 1));
})(document, window.posthog || []);
posthog.init("phc_xAvL2Iq4tFmANRE7kzbKwaSqp1HJjN7x48s3vr0CMjs", {
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well,
session_recording: {
recordCrossOriginIframes: true,
capturePerformance: false,
},
});
</script>
</body>
</html>
+1 -1
View File
@@ -29,7 +29,7 @@ export const Navbar = () => {
<div className="px-6 md:px-12 py-4 flex items-center justify-between">
<div className="flex items-center gap-10">
<Link to="/browse" className="flex items-center gap-2" data-testid="nav-logo">
<span className="font-display text-2xl font-black tracking-tighter text-white">Kino</span>
<span className="font-display text-2xl font-black tracking-tighter text-white">StreamHoard</span>
<span className="text-[#D9381E] text-2xl leading-none">.</span>
</Link>
{user && (
+1 -1
View File
@@ -61,7 +61,7 @@ export default function AdminShowEpisodes() {
const { data } = await api.post("/episodes/bulk-hide", { episode_ids: ids, hidden: false });
msg = `${data.modified} unhidden`;
} else if (action === "delete") {
if (!window.confirm(`Delete ${ids.length} episode(s) permanently? Files on disk are kept, but Kino entries and HLS output are removed.`)) return;
if (!window.confirm(`Delete ${ids.length} episode(s) permanently? Files on disk are kept, but StreamHoard entries and HLS output are removed.`)) return;
const { data } = await api.post("/episodes/bulk-delete", { episode_ids: ids });
msg = `${data.deleted} deleted`;
}
+2 -2
View File
@@ -48,7 +48,7 @@ export default function Login() {
<div className="flex items-center justify-center px-6 md:px-12 py-12">
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="login-form">
<Link to="/" className="block mb-12" data-testid="login-logo">
<span className="font-display text-3xl font-black tracking-tighter text-white">Kino</span>
<span className="font-display text-3xl font-black tracking-tighter text-white">StreamHoard</span>
<span className="text-[#D9381E] text-3xl">.</span>
</Link>
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Sign in</h2>
@@ -93,7 +93,7 @@ export default function Login() {
<div className="mt-8 p-4 border border-[#222] text-xs text-[#8A8A8A]" data-testid="login-demo-credentials">
<span className="text-[10px] uppercase tracking-[0.3em] text-[#D9381E] block mb-2">Demo Admin</span>
admin@kino.local / kino-admin-2026
admin@streamhoard.local / kino-admin-2026
</div>
</form>
</div>
+2 -2
View File
@@ -20,7 +20,7 @@ export default function Register() {
setSubmitting(true);
try {
await register(email, password, name);
toast.success("Welcome to Kino");
toast.success("Welcome to StreamHoard");
nav("/browse", { replace: true });
} catch (err) {
toast.error(err.response?.data?.detail || "Could not register");
@@ -49,7 +49,7 @@ export default function Register() {
<div className="flex items-center justify-center px-6 md:px-12 py-12">
<form onSubmit={onSubmit} className="w-full max-w-sm fade-up" data-testid="register-form">
<Link to="/" className="block mb-12">
<span className="font-display text-3xl font-black tracking-tighter text-white">Kino</span>
<span className="font-display text-3xl font-black tracking-tighter text-white">StreamHoard</span>
<span className="text-[#D9381E] text-3xl">.</span>
</Link>
<h2 className="font-display text-3xl font-bold tracking-tight text-white">Create account</h2>
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Kino — one-line installer
# StreamHoard — one-line installer
#
# Usage (interactive):
# curl -fsSL https://raw.githubusercontent.com/YOU/kino-media-server/main/install.sh | sudo bash
@@ -25,7 +25,7 @@ KINO_DIR="${KINO_DIR:-/opt/kino}"
KINO_HTTP_PORT="${KINO_HTTP_PORT:-8080}"
MEDIA_ROOT_HOST="${MEDIA_ROOT_HOST:-${KINO_DIR}/media}"
DB_NAME="${DB_NAME:-kino}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@kino.local}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@streamhoard.local}"
ADMIN_NAME="${ADMIN_NAME:-Admin}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
JWT_SECRET="${JWT_SECRET:-}"
@@ -172,7 +172,7 @@ fi
# ---------- Done ----------
cat <<EOF
$(c_grn '━━━ Kino is up ━━━')
$(c_grn '━━━ StreamHoard is up ━━━')
URL : $(c_cyn "$URL")
Admin : $(c_cyn "$ADMIN_EMAIL")