diff --git a/.env.example b/.env.example index 154a306..9083283 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,13 @@ JWT_SECRET=replace-with-a-secure-random-string # --------------------------------------------------------------------------- # Container Images # --------------------------------------------------------------------------- -# Tag for the locally built images +# Built from source by default. To pull published images instead, set the +# registry path they live under (ending in /), then: +# docker compose pull && docker compose up -d +# IMAGE_PREFIX=registry.example.com/owner/ # IMAGE_TAG=latest +# "Latest release" API URL to check for updates (GitHub or Gitea); off when unset +# APP_UPDATE_URL=https://git.example.com/api/v1/repos/owner/dotcom-arcade/releases/latest # --------------------------------------------------------------------------- # Port (host mapping) @@ -100,14 +105,12 @@ ENABLE_CACHE_PREWARM=true # --------------------------------------------------------------------------- # Application # --------------------------------------------------------------------------- -# HOME_RECENT_HOURS=24 # ENABLE_CGI=false # Streamed play for games that can't run in a browser (Java, Shockwave, the # original Flash Player, Windows games). Set a token (e.g. `openssl rand -hex 32`) # and enable the profile. See docs/10-features/13-streaming.md. # COMPOSE_PROFILES=streaming # STREAMING_API_TOKEN= -# STREAM_PORT=8081 # MAX_SESSIONS=4 # Sign in users an authenticating reverse proxy (e.g. Authentik forward auth) # names in a header. Off unless TRUSTED_HEADER_USERNAME and TRUSTED_PROXIES are diff --git a/.gitea/workflows/images.yml b/.gitea/workflows/images.yml new file mode 100644 index 0000000..f2e36bd --- /dev/null +++ b/.gitea/workflows/images.yml @@ -0,0 +1,65 @@ +# Builds the app, session manager and streamer images and pushes them to this +# Gitea instance's container registry, under the repository's owner. +# +# Needs repository secrets REGISTRY_USERNAME and REGISTRY_TOKEN: a user, and an +# access token of theirs with package write access. +name: Images + +on: + push: + branches: [main] + tags: ['v*.*.*'] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image: dotcom-arcade + context: . + - image: dotcom-arcade-session-manager + context: streaming/session-manager + - image: dotcom-arcade-streamer + context: streaming/streamer + steps: + - uses: actions/checkout@v4 + + # The registry lives on the same host as this Gitea instance + - name: Locate the registry + id: registry + run: | + host=${GITHUB_SERVER_URL#*://} + echo "host=${host%/}" >> "$GITHUB_OUTPUT" + echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ${{ steps.registry.outputs.host }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/${{ matrix.image }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=ref,event=branch + + - uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64 + build-args: | + VERSION=${{ steps.meta.outputs.version }} + provenance: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2109efa..e539373 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: true contact_links: - - name: Ask a Question - url: https://github.com/darkraise/flashpoint-web/discussions - about: Use Discussions for questions, help, and general conversation - name: Flashpoint Archive url: https://github.com/FlashpointProject about: For issues with Flashpoint itself (not this web interface) diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index 4678e9e..0000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,204 +0,0 @@ -# GitHub Actions Workflows - -This directory contains automated workflows for the Flashpoint Web project. - -## Workflows - -### 1. Docker Build and Push (`docker-build-push.yml`) - -Builds and pushes the single Docker image to the container registry. - -#### Triggers - -- **Version tags** (`v*.*.*`): Builds and pushes the image with version tags and `latest` tag -- **Manual dispatch**: Can be triggered manually from GitHub Actions tab - -#### Image Built - -- `flashpoint-web` - Single image serving both the REST API (including game - content) and the React web UI - -#### Container Registry - -The workflow pushes images to **Docker Hub**. - -#### Setup Instructions - -##### Docker Hub (Required) - -1. **Create Docker Hub account** at https://hub.docker.com - -2. **Create access token**: - - Go to Account Settings → Security → Access Tokens - - Click "New Access Token" - - Name: "GitHub Actions" - - Permissions: Read & Write - - Copy the token (you won't see it again!) - -3. **Add GitHub Secrets**: - - Go to your repository → Settings → Secrets and variables → Actions - - Click "New repository secret" - - Add two secrets: - - `DOCKERHUB_USERNAME`: Your Docker Hub username - - `DOCKERHUB_TOKEN`: The access token from step 2 - -4. **Image will be pushed to**: - ``` - docker.io/darkraise/flashpoint-web:latest - ``` - -#### Image Tags - -The workflow automatically creates multiple tags when you push a version tag: - -| Tag Type | Example | Description | -|----------|---------|-------------| -| `latest` | `latest` | Always created for every version tag | -| Full version | `1.2.3` | Git tag `v1.2.3` → `1.2.3` | -| Major.Minor | `1.2` | Git tag `v1.2.3` → `1.2` | -| Major | `1` | Git tag `v1.2.3` → `1` | - -#### Creating a Release - -To create a versioned release: - -```bash -# Ensure you're on master branch -git checkout master - -# Tag the commit -git tag v1.0.0 - -# Push the tag (this triggers the workflow) -git push origin v1.0.0 -``` - -This will create images tagged as: -- `1.0.0` -- `1.0` -- `1` -- `latest` - -#### Platform Support - -Images are built for: -- `linux/amd64` (x86_64) - Standard servers and desktops - -**Note:** ARM64 support has been disabled to significantly reduce build times. If you need ARM64 images, you can build them locally or enable multi-platform builds by adding `linux/arm64` to the platforms in the workflow file. - -#### Build Caching - -The workflow uses GitHub Actions cache to speed up builds: -- Layer cache stored between runs -- Significantly faster builds after the first run -- Cache automatically invalidated when dependencies change - -#### Usage Examples - -##### Pull and Run the Image - -```bash -# Pull the latest image -docker pull darkraise/flashpoint-web:latest - -# Or use a specific version -docker pull darkraise/flashpoint-web:1.0.0 -``` - -##### Use in Docker Compose - -Update your `docker-compose.yml` to use the pre-built image: - -```yaml -services: - flashpoint-web: - image: darkraise/flashpoint-web:latest - # Remove build section - ports: - - "80:3100" - # ... rest of config -``` - -Then simply run: -```bash -docker compose pull # Pull the latest image -docker compose up -d # Start the service -``` - -#### Making Images Public - -By default, Docker Hub images are private. To make them public: - -1. Go to https://hub.docker.com/repositories -2. Click on the repository (e.g., `flashpoint-web`) -3. Click "Settings" -4. Change visibility to "Public" - -#### Troubleshooting - -##### Build Fails - -Check the Actions tab for detailed error logs: -``` -https://github.com///actions -``` - -##### Docker Hub Not Pushing - -Verify secrets are set correctly: -- Repository → Settings → Secrets and variables → Actions -- Check `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` exist - -##### Image Not Found - -For private images, authenticate first: -```bash -docker login docker.io -``` - -#### Performance - -Typical build times (optimized for amd64 only): -- First build: 3-5 minutes -- Subsequent builds (with cache): 1-2 minutes -- Frontend and backend build stages run within a single image build - -**Optimizations applied:** -- Single platform (amd64) instead of multi-platform -- npm ci with `--prefer-offline` and `--no-audit` flags -- Disabled provenance and SBOM generation -- Latest BuildKit image -- GitHub Actions cache for Docker layers - -#### Security - -- Secrets are never exposed in logs -- Images are scanned for vulnerabilities (optional - can add) -- Multi-stage builds minimize attack surface -- Non-root users in all containers -- Read-only root filesystems where possible - ---- - -### 2. Performance Budget (`performance-budget.yml`) - -Frontend performance monitoring workflow. - -See the workflow file for details. - ---- - -## Adding New Workflows - -To add a new workflow: - -1. Create a new YAML file in `.github/workflows/` -2. Use the GitHub Actions syntax -3. Test locally with [act](https://github.com/nektos/act) (optional) -4. Push to trigger the workflow - -## References - -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [Docker Build Push Action](https://github.com/docker/build-push-action) -- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml deleted file mode 100644 index 2dc3d92..0000000 --- a/.github/workflows/docker-build-push.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Build and Push Docker Images - -on: - push: - tags: - - 'v*.*.*' - workflow_dispatch: - -env: - REGISTRY_DOCKERHUB: docker.io - -jobs: - build-and-push: - runs-on: ubuntu-latest - timeout-minutes: 30 - - permissions: - contents: read - - outputs: - version: ${{ steps.meta.outputs.version }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - driver-opts: | - image=moby/buildkit:latest - network=host - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY_DOCKERHUB }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY_DOCKERHUB }}/${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web - tags: | - # Tag with full version (v1.0.0 -> 1.0.0) - type=semver,pattern={{version}} - # Tag with major.minor (v1.0.0 -> 1.0) - type=semver,pattern={{major}}.{{minor}} - # Tag with major version (v1.0.0 -> 1) - type=semver,pattern={{major}} - # Latest tag - type=raw,value=latest - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=flashpoint-web - cache-to: type=gha,scope=flashpoint-web,mode=max - platforms: linux/amd64 - build-args: | - BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} - VCS_REF=${{ github.sha }} - VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} - provenance: false - sbom: false - - summary: - needs: build-and-push - runs-on: ubuntu-latest - if: always() - steps: - - name: Build Summary - run: | - echo "## Docker Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Image Built:" >> $GITHUB_STEP_SUMMARY - echo "- \`flashpoint-web\` (API + UI)" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Registry:" >> $GITHUB_STEP_SUMMARY - echo "- Docker Hub: \`${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Pull Command:" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web:latest" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - create-release: - needs: build-and-push - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') - - permissions: - contents: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version from tag - id: version - run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Generate release notes - id: release-notes - run: | - # Get the previous tag - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") - - echo "## Docker Image" >> release_notes.md - echo "" >> release_notes.md - echo "Pull the image from Docker Hub:" >> release_notes.md - echo "" >> release_notes.md - echo "\`\`\`bash" >> release_notes.md - echo "docker pull ${{ secrets.DOCKERHUB_USERNAME }}/flashpoint-web:${{ steps.version.outputs.version }}" >> release_notes.md - echo "\`\`\`" >> release_notes.md - echo "" >> release_notes.md - - if [ -n "$PREV_TAG" ]; then - echo "## Changes since $PREV_TAG" >> release_notes.md - echo "" >> release_notes.md - git log --pretty=format:"- %s (%h)" $PREV_TAG..HEAD >> release_notes.md - echo "" >> release_notes.md - fi - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.version.outputs.version }} - name: Release ${{ steps.version.outputs.version }} - body_path: release_notes.md - draft: false - prerelease: ${{ contains(steps.version.outputs.version, '-') }} - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index b08cec0..7232ae0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1193,13 +1193,8 @@ Before submitting code, verify: ## Environment ```bash -FLASHPOINT_PATH=D:/Flashpoint # Required - path to Flashpoint installation +FLASHPOINT_PATH=/path/to/flashpoint # Required - path to Flashpoint installation JWT_SECRET=your-secret # Required - random string for JWT signing DOMAIN=http://localhost:5173 # Optional - frontend URL for CORS LOG_LEVEL=info # Optional - debug, info, warn, error ``` - -## Source References - -- Flashpoint Launcher: `D:\Repositories\Community\launcher` -- Flashpoint Game Server: `D:\Repositories\Community\FlashpointGameServer` diff --git a/README.md b/README.md index d41c4c0..158fad2 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,21 @@ Flash Player stream from your server. > Flashpoint Archive. It's built on > [flashpoint-web](https://github.com/darkraise/flashpoint-web) by darkraise. +## Before you start: hosting requirements + +> [!WARNING] +> +> **Dotcom Arcade needs a lot of disk, and streaming needs a lot of CPU.** Check +> these before you download anything. + +| Resource | Needed | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Storage** | **More than 2.3 TB** for Flashpoint Ultimate 14, and growing with each release. 4.6 TB if you keep the downloaded archives to seed them. A spare desktop drive won't do. | +| **Time** | A day or more to download and extract, depending on your connection and disks. | +| **Server** | Linux with Docker. 2 CPU cores and 4 GB of RAM serve the site and in-browser games to a household. | +| **Streaming** | Optional, and heavy: every player gets their own container running the game and encoding its video on the CPU. Plan on roughly 1 to 2 CPU cores and 1 GB of RAM per player streaming at once, and a few Mbps of upload each. | +| **HTTPS** | Required for streaming, since browsers only allow its video decoder on secure pages. Recommended anyway. | + ## Features - **Plays almost everything.** @@ -54,27 +69,50 @@ Flash Player stream from your server. ## Getting started -You need a Linux server with Docker, and enough disk for Flashpoint Ultimate: -about 2.3 TB for version 14, or 4.6 TB if you keep the downloaded archives to -seed. Expect the download and extraction to take a day or more. +Check the [hosting requirements](#before-you-start-hosting-requirements) first. ### 1. Get Flashpoint Ultimate -[`scripts/setup-flashpoint.sh`](scripts/setup-flashpoint.sh) downloads the -torrent with `aria2c` and then extracts each archive in turn. It checks free -space before each one and deletes it afterwards, so the disk never needs room -for both. It also pulls the database, settings and playlists out of -`Base_1.zip`, which otherwise only holds Windows software. Re-run it to resume -after an interruption. +Download two files from Flashpoint's +[downloads page](https://flashpointarchive.org/downloads): + +- the **Flashpoint Ultimate** `.torrent` file; +- **Flashpoint Infinity for Linux** (`.7z`), whose software streaming needs. + +[`scripts/setup-flashpoint.sh`](scripts/setup-flashpoint.sh) then does the rest: + +- It checks that you have the disk space, and stops before downloading anything + if you don't. +- It downloads the torrent with `aria2c`. +- It extracts each archive in turn and deletes it afterwards, so the disk never + needs room for both. +- It pulls the database, settings and playlists out of `Base_1.zip`, which + otherwise only holds Windows software. + +Re-run it to resume after an interruption. ```sh sudo apt install aria2 7zip tmux tmux new -s flashpoint -scripts/setup-flashpoint.sh --with-infinity /srv/flashpoint +scripts/setup-flashpoint.sh --torrent FlashpointUltimate14.torrent \ + --infinity fp14.0.3_lin_20251201.7z --seed-ratio 1.0 /srv/flashpoint ``` -`--with-infinity` adds Flashpoint Infinity for Linux's software, which streaming -needs. `--keep-archives` keeps the archives so you can keep seeding. +#### Please seed + +Flashpoint Ultimate is shared over BitTorrent, and stays available only as long +as people seed it. If you can, give back at least what you downloaded: + +- **Seed before extracting.** `--seed-ratio 1.0` keeps seeding after the + download until you've uploaded as much as you downloaded. `--seed-hours 24` + seeds for a day. With both, seeding stops at whichever comes first. This needs + no extra disk space, but extraction waits until seeding stops. +- **Seed for the long term.** `--keep-archives` keeps the archives after + extracting (about 4.6 TB in total), so a torrent client such as qBittorrent or + Transmission can go on seeding the downloads directory. +- **Let peers reach you.** The download listens on ports 6881-6999, TCP and UDP. + Forwarding them on your router and allowing them through your firewall lets + more people download from you. ### 2. Configure @@ -105,7 +143,7 @@ Under **Settings → App** you can set the site name and tagline. Streaming plays the games browsers can't. To turn it on: -1. Run the setup script with `--with-infinity`. +1. Run the setup script with `--infinity`. 2. Add to `.env`: ```sh @@ -114,13 +152,12 @@ Streaming plays the games browsers can't. To turn it on: ``` 3. Rebuild with `docker compose up -d --build`. -4. Route `/stream/` on your site to the session manager (port 8081). Streams - need HTTPS. See the [streaming guide](docs/10-features/13-streaming.md) for - an nginx example. +4. Serve the site over HTTPS. The app proxies streams itself, so your reverse + proxy only needs to pass WebSocket upgrades and allow long-lived connections. + See the [streaming guide](docs/10-features/13-streaming.md). The session manager starts containers through the Docker socket, which amounts -to root on the host. Keep it off the public internet. Only `/stream/` needs to -be reachable, and only through your proxy. +to root on the host. It publishes no ports: only the app talks to it. ### Signing in through a reverse proxy @@ -156,6 +193,15 @@ and development. Useful starting points: - [Selkies](https://github.com/selkies-project/selkies), which streams the games. +## Legal + +Flashpoint Archive is a preservation project whose legal standing has never been +settled. It removes games when their rights holders ask. Dotcom Arcade contains +no games and links to no game downloads. You get Flashpoint from Flashpoint's +own site, and you are responsible for complying with copyright law where you +live. Rights holders who want a game removed should contact +[Flashpoint Archive](https://flashpointarchive.org/). + ## License GPL-3.0, like flashpoint-web. See [LICENSE](LICENSE). diff --git a/backend/.env.example b/backend/.env.example index de0e0e3..6e79ec5 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -22,7 +22,6 @@ BCRYPT_SALT_ROUNDS=10 # ENABLE_CGI=false # Misc -HOME_RECENT_HOURS=24 # --------------------------------------------------------------------------- # Database Performance Optimization (for network storage) diff --git a/backend/src/config.ts b/backend/src/config.ts index 65726c2..b3fc24e 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -167,12 +167,12 @@ export const config = { bcryptSaltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS || '', 10) || 10, - homeRecentHours: parseInt(process.env.HOME_RECENT_HOURS || '', 10) || 24, - // Session manager that streams games which can't run in a browser, such as // Java applets (flashpoint-ultimate-server). Streaming is off unless both are set. streamingApiUrl: process.env.STREAMING_API_URL || null, streamingApiToken: process.env.STREAMING_API_TOKEN || null, + // Where /stream/ is proxied to: the session manager's stream port + streamingStreamUrl: process.env.STREAMING_STREAM_URL || null, // Sign users in from a header set by an authenticating reverse proxy, such as // Authentik forward auth. Off unless the header name and the proxy addresses @@ -208,6 +208,8 @@ export const config = { // marks the build as unreleased. `||` is deliberate: a whitespace-only value // must collapse to null, which `??` would not do. appVersion: process.env.APP_VERSION?.trim() || null, + // A GitHub- or Gitea-style "latest release" API URL; update checks are off without it + appUpdateUrl: process.env.APP_UPDATE_URL?.trim() || null, } as const; /** Resolves external image CDN URLs from Flashpoint preferences, with hardcoded fallbacks. */ diff --git a/backend/src/routes/github.ts b/backend/src/routes/github.ts deleted file mode 100644 index a426283..0000000 --- a/backend/src/routes/github.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Router, Request, Response } from 'express'; -import { logger } from '../utils/logger'; -import { asyncHandler } from '../middleware/asyncHandler'; - -const router = Router(); - -// In-memory cache for GitHub star count (avoid hitting 60/hr unauthenticated rate limit) -let cachedStars: { count: number; fetchedAt: number } | null = null; -const CACHE_TTL_MS = 15 * 60 * 1000; - -// Proxy to avoid browser CORS issues with GitHub API -router.get( - '/stars', - asyncHandler(async (_req: Request, res: Response) => { - if (cachedStars && Date.now() - cachedStars.fetchedAt < CACHE_TTL_MS) { - return res.json({ - success: true, - data: { stars: cachedStars.count }, - }); - } - - const repoUrl = 'https://api.github.com/repos/darkraise/flashpoint-web'; - - const response = await fetch(repoUrl, { - headers: { - Accept: 'application/vnd.github.v3+json', - 'User-Agent': 'Flashpoint-Web-App', - }, - }); - - if (!response.ok) { - // If rate-limited and we have a stale cache, return it - if (cachedStars) { - logger.debug('[GitHub] Rate limited, returning stale cache'); - return res.json({ - success: true, - data: { stars: cachedStars.count }, - }); - } - throw new Error(`GitHub API error: ${response.status}`); - } - - const data = (await response.json()) as { stargazers_count: number }; - - cachedStars = { count: data.stargazers_count, fetchedAt: Date.now() }; - - res.json({ - success: true, - data: { - stars: data.stargazers_count, - }, - }); - }) -); - -export default router; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index d8c242c..32524e9 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -25,7 +25,6 @@ import ratingsRouter from './ratings'; import gameSavesRouter from './game-saves'; import discoveryRouter from './discovery'; import ruffleRouter from './ruffle'; -import githubRouter from './github'; import domainsRouter from './domains'; import healthRouter from './health'; import cacheRouter from './cache'; @@ -40,7 +39,6 @@ export function setupRoutes(app: Express): void { app.use('/api/settings/auth', authSettingsRouter); app.use('/api/settings', systemSettingsRouter); app.use('/api/ruffle', ruffleRouter); - app.use('/api/github', githubRouter); app.use('/api/domains', domainsRouter); app.use('/api/jobs', jobsRouter); app.use('/api/users', usersRouter); diff --git a/backend/src/routes/streamProxy.test.ts b/backend/src/routes/streamProxy.test.ts new file mode 100644 index 0000000..02fb125 --- /dev/null +++ b/backend/src/routes/streamProxy.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import http, { type Server } from 'http'; +import type { AddressInfo } from 'net'; +import express from 'express'; +import { mountStreamProxy } from './streamProxy'; + +vi.mock('../utils/logger', () => ({ + logger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() }, +})); + +let upstream: Server; +let app: Server; +let base: string; + +function listen(server: Server): Promise { + return new Promise((resolve) => + server.listen(0, '127.0.0.1', () => resolve((server.address() as AddressInfo).port)) + ); +} + +beforeAll(async () => { + // A stand-in session manager: echoes the path, and accepts WebSocket upgrades + upstream = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/plain'); + res.end(`upstream saw ${req.method} ${req.url}`); + }); + upstream.on('upgrade', (req, socket) => { + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n' + ); + socket.write(`ws ${req.url}`); + socket.end(); + }); + const upstreamPort = await listen(upstream); + + const expressApp = express(); + const attach = mountStreamProxy(expressApp, `http://127.0.0.1:${upstreamPort}`); + expressApp.get('/other', (_req, res) => { + res.send('app'); + }); + app = http.createServer(expressApp); + attach(app); + base = `http://127.0.0.1:${await listen(app)}`; +}); + +afterAll(() => { + app.close(); + upstream.close(); +}); + +describe('stream proxy', () => { + it('forwards /stream/ requests with their full path', async () => { + const res = await fetch(`${base}/stream/abc/_fp/start`, { method: 'POST' }); + expect(await res.text()).toBe('upstream saw POST /stream/abc/_fp/start'); + }); + + it('leaves other routes to the app', async () => { + expect(await (await fetch(`${base}/other`)).text()).toBe('app'); + }); + + it('forwards WebSocket upgrades', async () => { + const received = await new Promise((resolve, reject) => { + const req = http.request(`${base}/stream/abc/websocket`, { + headers: { Connection: 'Upgrade', Upgrade: 'websocket' }, + }); + req.on('upgrade', (res, socket, head) => { + expect(res.statusCode).toBe(101); + let data = head.toString(); + socket.on('data', (chunk) => (data += chunk.toString())); + socket.on('end', () => resolve(data)); + }); + req.on('error', reject); + req.end(); + }); + expect(received).toBe('ws /stream/abc/websocket'); + }); + + it('answers 502 when the session manager is down', async () => { + const down = express(); + mountStreamProxy(down, 'http://127.0.0.1:9'); + const server = http.createServer(down); + const port = await listen(server); + const res = await fetch(`http://127.0.0.1:${port}/stream/abc/`); + server.close(); + expect(res.status).toBe(502); + }); +}); diff --git a/backend/src/routes/streamProxy.ts b/backend/src/routes/streamProxy.ts new file mode 100644 index 0000000..39c391f --- /dev/null +++ b/backend/src/routes/streamProxy.ts @@ -0,0 +1,40 @@ +import { ServerResponse, type IncomingMessage, type Server } from 'http'; +import type { Socket } from 'net'; +import type { Express } from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { logger } from '../utils/logger'; + +/** + * Proxies /stream/ to the session manager, WebSockets included, so a reverse + * proxy in front of the app needs no route of its own for streams. Returns a + * function that wires WebSocket upgrades into the HTTP server once it exists. + */ +export function mountStreamProxy(app: Express, target: string): (server: Server) => void { + const proxy = createProxyMiddleware('/stream/', { + target, + ws: true, + logLevel: 'silent', + // Save sync is a long-lived event stream; don't let the server's idle timeout cut it + onProxyReq: (_proxyReq, req) => { + req.socket.setTimeout(0); + }, + onError: (error, req, res) => { + logger.warn('[Streaming] Stream proxy error', { url: req.url, error: error.message }); + // A failed WebSocket upgrade hands over the raw socket instead of a response + if (!(res instanceof ServerResponse)) { + (res as Socket).destroy(); + } else if (!res.headersSent) { + res.writeHead(502).end(); + } + }, + }); + app.use(proxy); + + return (server) => { + server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) => { + if (!req.url?.startsWith('/stream/')) return; + socket.setTimeout(0); + proxy.upgrade?.(req as Parameters>[0], socket, head); + }); + }; +} diff --git a/backend/src/routes/system-settings.ts b/backend/src/routes/system-settings.ts index b3afd0e..1044173 100644 --- a/backend/src/routes/system-settings.ts +++ b/backend/src/routes/system-settings.ts @@ -80,11 +80,6 @@ router.get( Record > = systemSettings.getPublicSettings(); - if (!publicSettings.app) { - publicSettings.app = {}; - } - publicSettings.app.homeRecentHours = config.homeRecentHours; - // Inject edition/version from config (auto-detected from version.txt, not stored in DB) if (!publicSettings.metadata) { publicSettings.metadata = {}; diff --git a/backend/src/routes/updates.test.ts b/backend/src/routes/updates.test.ts index c51e819..1c47ed2 100644 --- a/backend/src/routes/updates.test.ts +++ b/backend/src/routes/updates.test.ts @@ -51,7 +51,7 @@ const INFO = { updateAvailable: true, isUnreleasedBuild: false, publishedAt: '2026-08-12T07:11:45Z', - releaseUrl: 'https://github.com/darkraise/flashpoint-web/releases/tag/v1.0.42', + releaseUrl: 'https://git.example.com/owner/dotcom-arcade/releases/tag/v1.0.42', changelog: '## Changes', checkedAt: '2026-08-12T08:00:00.000Z', lastCheckFailed: false, diff --git a/backend/src/server.ts b/backend/src/server.ts index 4a0bfa4..2c6217d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -44,6 +44,8 @@ import { zipManager } from './game/zip-manager'; import { gameZipServer } from './game/gamezipserver'; import gameProxyRouter from './routes/game-proxy'; import gameZipRouter from './routes/game-zip'; +import { mountStreamProxy } from './routes/streamProxy'; +import { StreamingService } from './services/StreamingService'; /** * Compute SHA-256 CSP hashes for inline