Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .claude/learnings/docker-dns-casing-2025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Docker DNS Case Sensitivity

**Date:** 2025-12-22
**Source:** PR #345 CodeRabbit review

## Issue

Mixed hostname casing in docker-compose.yml caused inconsistent DNS resolution:
- `supabase_kong_PMOVES.AI` (uppercase)
- `supabase_kong_pmoves.ai` (lowercase)

Docker DNS behavior varies by configuration - some resolve case-insensitively, others don't.

## Pattern

**Always use lowercase for Docker hostnames.**

```yaml
# Bad - inconsistent casing
- SUPABASE_URL=http://supabase_kong_PMOVES.AI:8000
- SUPABASE_REALTIME_URL=ws://supabase_kong_PMOVES.AI:8000/realtime/v1

# Good - consistent lowercase
- SUPABASE_URL=http://supabase_kong_pmoves.ai:8000
- SUPABASE_REALTIME_URL=ws://supabase_kong_pmoves.ai:8000/realtime/v1
```

## Detection

```bash
# Find mixed casing in docker-compose files
grep -n "supabase_kong" pmoves/docker-compose.yml | sort -u
```

## Fix

Use `replace_all` to normalize:
```bash
sed -i 's/supabase_kong_PMOVES\.AI/supabase_kong_pmoves.ai/g' pmoves/docker-compose.yml
```

## Related

- `.claude/learnings/docker-entrypoint-patterns-2025.md`
60 changes: 60 additions & 0 deletions .claude/learnings/dockerfile-permissions-2025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Dockerfile Non-Root Container Permissions

**Date:** 2025-12-22
**Source:** PR #345 CodeRabbit review

## Issue

Dockerfile hardening added user creation but missed creating directories
needed by entrypoint scripts:
- entrypoint.sh writes to `/models` via `snapshot_download()`
- Directory doesn't exist or has wrong ownership

## Pattern

**Create and chown all directories written to at runtime BEFORE USER directive:**

```dockerfile
# Security: Run as non-root user
RUN groupadd -r pmoves --gid=65532 && \
useradd -r -g pmoves --uid=65532 --home-dir=/app --shell=/sbin/nologin pmoves && \
mkdir -p /models && \
chown -R pmoves:pmoves /app /app/vibevoice /models

USER pmoves
```

## Checklist for Dockerfile Hardening

1. **Identify write paths** - grep entrypoint/startup scripts for:
- File writes
- `mkdir` commands
- Model downloads (HuggingFace, etc.)
- Log directories
- Cache directories

2. **Create directories** - Add `mkdir -p` for each path

3. **Set ownership** - Add to `chown -R pmoves:pmoves` list

4. **Order matters** - All `mkdir` and `chown` must be BEFORE `USER pmoves`

## Detection

```bash
# Find write operations in entrypoint scripts
grep -E "mkdir|>|>>|download|cache" services/*/entrypoint.sh
```

## Anti-Pattern

```dockerfile
# Bad - USER before mkdir means mkdir runs as non-root
USER pmoves
RUN mkdir -p /models # Permission denied!
```

## Related

- `.claude/learnings/docker-entrypoint-patterns-2025.md`
- PMOVES Dockerfile hardening standard (UID/GID 65532)
46 changes: 46 additions & 0 deletions .claude/learnings/nextjs-env-vars-2025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Next.js NEXT_PUBLIC Environment Variables

**Date:** 2025-12-22
**Source:** PR #345 CodeRabbit review

## Issue

`NEXT_PUBLIC_*` env vars were set to internal Docker hostnames:
```yaml
- NEXT_PUBLIC_FLUTE_GATEWAY_URL=http://flute-gateway:8055
- NEXT_PUBLIC_FLUTE_WS_URL=ws://flute-gateway:8056
```

These hostnames resolve inside Docker network but **fail in browser** because:
1. `NEXT_PUBLIC_*` vars are embedded at **build time**
2. Browser JavaScript runs **client-side**, outside Docker network
3. Browser cannot resolve internal Docker DNS names

## Pattern

**Use env var overrides with localhost fallback for client-side URLs:**

```yaml
# Good - allows override, defaults to host-accessible localhost
- NEXT_PUBLIC_FLUTE_GATEWAY_URL=${NEXT_PUBLIC_FLUTE_GATEWAY_URL:-http://localhost:8055}
- NEXT_PUBLIC_FLUTE_WS_URL=${NEXT_PUBLIC_FLUTE_WS_URL:-ws://localhost:8056}
```

## Server-Side vs Client-Side

| Variable | Scope | Can use Docker hostname? |
|----------|-------|-------------------------|
| `FLUTE_GATEWAY_URL` | Server-side | Yes |
| `NEXT_PUBLIC_FLUTE_GATEWAY_URL` | Client-side | No - use localhost |

## Detection

```bash
# Find NEXT_PUBLIC vars with Docker hostnames
grep "NEXT_PUBLIC.*http://[a-z-]*:" pmoves/docker-compose.yml
```

## Related

- Next.js documentation on environment variables
- `.claude/context/flute-gateway.md`
53 changes: 53 additions & 0 deletions .claude/learnings/prometheus-registry-2025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Prometheus Registry Double-Registration

**Date:** 2025-12-22
**Source:** PR #345 CodeRabbit review

## Issue

Direct access to `REGISTRY._names_to_collectors` is fragile:
1. Private API may change across prometheus_client versions
2. No type validation if metric with same name but different type exists
3. No labelnames validation

## Pattern

**Use try/except with type validation instead of direct private API access:**

```python
from prometheus_client import Counter, REGISTRY

def _get_or_create_counter(name: str, description: str, labelnames: tuple) -> Counter:
"""Get existing counter or create new one (safe for module reimport via -m flag)."""
try:
return Counter(name, description, labelnames=labelnames)
except ValueError as e:
if "Duplicated timeseries" in str(e):
# Metric already exists; retrieve and validate type
existing = REGISTRY._names_to_collectors.get(name)
if existing is not None and isinstance(existing, Counter):
return existing
raise TypeError(
f"Metric '{name}' exists but is {type(existing).__name__}, not Counter"
) from e
raise
Comment on lines +20 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Address labelnames mismatch scenarios.

The function accepts a labelnames tuple but does not validate that an existing metric's labelnames match. If a metric with the same name but different labelnames exists, isinstance(existing, Counter) will pass, but the returned metric may have incompatible labelnames. Consider documenting:

  • Whether prometheus_client raises an error for labelnames mismatches, or
  • Adding explicit labelnames validation when retrieving an existing metric.
🤖 Prompt for AI Agents
In .claude/learnings/prometheus-registry-2025.md around lines 20 to 33, the
helper returns an existing Counter without verifying its labelnames, which can
silently return a metric with incompatible labels; when retrieving the existing
collector, compare the provided labelnames tuple to the collector's labelnames
(e.g., existing._labelnames or existing._labelnames property) and if they differ
raise a clear TypeError indicating the name exists with different labelnames, or
alternatively log the mismatch and re-raise ValueError; also add a short comment
documenting the chosen behavior and why labelname validation is required.

```

## Why This Matters

The `python -m` flag can cause double module import, registering metrics twice.
This helper safely handles the duplicate registration case while validating types.

## Anti-Pattern

```python
# Bad - no type validation, silent bugs if metric type differs
if name in REGISTRY._names_to_collectors:
return REGISTRY._names_to_collectors[name]
return Counter(name, description, labelnames=labelnames)
```

## Related

- prometheus_client library documentation
- Python `-m` flag double-import behavior
50 changes: 50 additions & 0 deletions pmoves/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,56 @@ services:
extra_hosts:
- "host.docker.internal:host-gateway"

# PMOVES.AI Unified Portal - Main Dashboard
pmoves-ui:
build:
context: ./ui
dockerfile: Dockerfile
restart: unless-stopped
env_file: [ env.shared.generated, env.shared, .env.generated, .env.local ]
environment:
- NODE_ENV=production
# Supabase
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL:-http://localhost:8000}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
- NEXT_PUBLIC_SUPABASE_REST_URL=${NEXT_PUBLIC_SUPABASE_REST_URL:-http://localhost:3010}
# Voice/TTS
- NEXT_PUBLIC_FLUTE_GATEWAY_URL=${NEXT_PUBLIC_FLUTE_GATEWAY_URL:-http://localhost:8055}
- NEXT_PUBLIC_FLUTE_WS_URL=${NEXT_PUBLIC_FLUTE_WS_URL:-ws://localhost:8056}
# Agent Services (client-side needs localhost)
- NEXT_PUBLIC_AGENT_ZERO_URL=${NEXT_PUBLIC_AGENT_ZERO_URL:-http://localhost:8080}
- NEXT_PUBLIC_AGENT_ZERO_UI_URL=${NEXT_PUBLIC_AGENT_ZERO_UI_URL:-http://localhost:8081}
- NEXT_PUBLIC_ARCHON_URL=${NEXT_PUBLIC_ARCHON_URL:-http://localhost:8091}
- NEXT_PUBLIC_ARCHON_UI_URL=${NEXT_PUBLIC_ARCHON_UI_URL:-http://localhost:3737}
# Knowledge & Search
- NEXT_PUBLIC_HIRAG_URL=${NEXT_PUBLIC_HIRAG_URL:-http://localhost:8086}
- NEXT_PUBLIC_OPEN_NOTEBOOK_API_URL=${NEXT_PUBLIC_OPEN_NOTEBOOK_API_URL:-http://localhost:8503}
- NEXT_PUBLIC_TENSORZERO_UI_URL=${NEXT_PUBLIC_TENSORZERO_UI_URL:-http://localhost:4000}
# Media & Ingestion
- NEXT_PUBLIC_PRESIGN_URL=${NEXT_PUBLIC_PRESIGN_URL:-http://localhost:8088}
- NEXT_PUBLIC_PMOVES_YT_BASE_URL=${NEXT_PUBLIC_PMOVES_YT_BASE_URL:-http://localhost:8077}
- NEXT_PUBLIC_CHANNEL_MONITOR_STATS_URL=${NEXT_PUBLIC_CHANNEL_MONITOR_STATS_URL:-http://localhost:8097/api/monitor/stats}
# Monitoring Stack
- NEXT_PUBLIC_GRAFANA_URL=${NEXT_PUBLIC_GRAFANA_URL:-http://localhost:3002}
- NEXT_PUBLIC_PROMETHEUS_URL=${NEXT_PUBLIC_PROMETHEUS_URL:-http://localhost:9090}
- NEXT_PUBLIC_LOKI_URL=${NEXT_PUBLIC_LOKI_URL:-http://localhost:3100}
# Server-side only (Docker hostnames OK)
- POSTGREST_URL=http://postgrest:3000
- PRESIGN_SHARED_SECRET=${PRESIGN_SHARED_SECRET}
- RENDER_WEBHOOK_URL=${RENDER_WEBHOOK_URL:-http://render-webhook:8085/comfy/webhook}
- OPEN_NOTEBOOK_API_URL=${OPEN_NOTEBOOK_API_URL:-http://open-notebook:8503}
ports:
- "4482:3000"
# Note: flute-gateway is optional (for voice features). Start with workers profile for full functionality.
networks: [ app_tier, api_tier, supabase_net ]
profiles: [ "ui" ]
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s

ffmpeg-whisper:
build:
context: ..
Expand Down
3 changes: 2 additions & 1 deletion pmoves/services/vibevoice-realtime/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ RUN chmod +x /entrypoint.sh
# Security: Run as non-root user
RUN groupadd -r pmoves --gid=65532 && \
useradd -r -g pmoves --uid=65532 --home-dir=/app --shell=/sbin/nologin pmoves && \
chown -R pmoves:pmoves /app /app/vibevoice
mkdir -p /models && \
chown -R pmoves:pmoves /app /app/vibevoice /models

USER pmoves

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand Down
50 changes: 50 additions & 0 deletions pmoves/ui/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
FROM node:20-slim AS builder

LABEL org.opencontainers.image.source="https://github.com/POWERFULMOVES/PMOVES.AI"
LABEL org.opencontainers.image.description="PMOVES.AI Unified Portal - Next.js Dashboard"

WORKDIR /app

# Install curl for health checks
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy source and build
COPY . .
RUN npm run build

# Production image
FROM node:20-slim AS runner
WORKDIR /app

# Install curl for health checks
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*

# Security: Non-root user (consistent with other PMOVES services)
RUN groupadd -r pmoves --gid=65532 && \
useradd -r -g pmoves --uid=65532 --home-dir=/app --shell=/sbin/nologin pmoves

# Copy built assets from builder
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

# Change ownership to non-root user
RUN chown -R pmoves:pmoves /app

USER pmoves

ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -sf http://localhost:3000/api/health || exit 1

# Next.js standalone server
CMD ["node", "server.js"]
14 changes: 14 additions & 0 deletions pmoves/ui/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextResponse } from 'next/server';

/**
* Health check endpoint for container orchestration.
* Returns service status, name, and timestamp.
*/
export async function GET() {
return NextResponse.json({
status: 'healthy',
service: 'pmoves-ui',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '0.1.0',
});
}
Loading
Loading