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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ __pycache__
.pytest_cache

# Node modules (installed fresh in build)
node_modules
**/node_modules

# IDE and OS files
.vscode
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Get short commit hash
id: commit
run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
Expand All @@ -33,5 +37,9 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
tags: speedarr/speedarr:develop
build-args: |
SPEEDARR_VERSION=develop
SPEEDARR_COMMIT=${{ steps.commit.outputs.short_sha }}
SPEEDARR_BRANCH=develop
cache-from: type=gha
cache-to: type=gha,mode=max
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
Expand All @@ -45,6 +49,10 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SPEEDARR_VERSION=${{ steps.version.outputs.version }}
SPEEDARR_COMMIT=${{ github.sha }}
SPEEDARR_BRANCH=main
cache-from: type=gha
cache-to: type=gha,mode=max

Expand Down
25 changes: 25 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
# Speedarr Single-Container Dockerfile with Frontend

# Build args (declared globally, re-declared per stage as needed)
ARG SPEEDARR_VERSION=dev
ARG SPEEDARR_COMMIT=unknown
ARG SPEEDARR_BRANCH=unknown

# Stage 1: Build React Frontend
FROM node:20.11-alpine3.19 AS frontend-builder

# Re-declare ARGs for this stage
ARG SPEEDARR_VERSION
ARG SPEEDARR_COMMIT
ARG SPEEDARR_BRANCH

# Set as env vars for Vite define (read at build time)
ENV VITE_APP_VERSION=${SPEEDARR_VERSION}
ENV VITE_APP_COMMIT=${SPEEDARR_COMMIT}
ENV VITE_APP_BRANCH=${SPEEDARR_BRANCH}

WORKDIR /frontend

# Copy package files
Expand All @@ -20,6 +35,16 @@ RUN npm run build
# Stage 2: Python Backend + Serve Frontend
FROM python:3.11.7-slim-bookworm AS base

# Re-declare ARGs for this stage
ARG SPEEDARR_VERSION
ARG SPEEDARR_COMMIT
ARG SPEEDARR_BRANCH

# Set as env vars for backend runtime (os.getenv)
ENV SPEEDARR_VERSION=${SPEEDARR_VERSION}
ENV SPEEDARR_COMMIT=${SPEEDARR_COMMIT}
ENV SPEEDARR_BRANCH=${SPEEDARR_BRANCH}

# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
Expand Down
6 changes: 5 additions & 1 deletion backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
Speedarr - Intelligent bandwidth management for Plex and download clients.
"""

__version__ = "0.1.0"
import os

__version__ = os.getenv("SPEEDARR_VERSION", "dev")
__commit__ = os.getenv("SPEEDARR_COMMIT", "unknown")
__branch__ = os.getenv("SPEEDARR_BRANCH", "unknown")
2 changes: 1 addition & 1 deletion backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ class Settings(BaseSettings):

# Application
app_name: str = "Speedarr"
app_version: str = "0.1.0"
app_version: str = Field(default_factory=lambda: __import__('app').__version__)
debug: bool = False

# Server
Expand Down
7 changes: 5 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi.responses import FileResponse
from loguru import logger

from app import __version__, __commit__, __branch__
from app.config import settings, SpeedarrConfig
from app.middleware.correlation import CorrelationIdMiddleware
from app.constants import (
Expand Down Expand Up @@ -296,7 +297,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Speedarr",
description="Intelligent bandwidth management for Plex and download clients",
version="0.1.0",
version=__version__,
lifespan=lifespan
)

Expand Down Expand Up @@ -332,7 +333,9 @@ async def health_check():
"""Combined health check endpoint (no auth required)."""
return {
"status": "healthy",
"version": "0.1.0"
"version": __version__,
"commit": __commit__,
"branch": __branch__,
}


Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class ApiClient {
return this.deduplicatedGet<SystemStatus>('/status/current');
}

async getHealth(): Promise<{ status: string; version: string }> {
async getHealth(): Promise<{ status: string; version: string; commit: string; branch: string }> {
const response = await this.client.get('/status/health');
return response.data;
}
Expand Down
209 changes: 117 additions & 92 deletions frontend/src/components/settings/SystemSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, AlertCircle, CheckCircle, Download } from 'lucide-react';
import { Loader2, AlertCircle, CheckCircle, Download, Info } from 'lucide-react';
import {
Select,
SelectContent,
Expand Down Expand Up @@ -117,6 +117,10 @@ export const SystemSettings: React.FC = () => {
}
};

const versionDisplay = __APP_VERSION__ === 'develop'
? `develop (${__APP_COMMIT__})`
: __APP_VERSION__;

if (isLoading) {
return (
<Card>
Expand All @@ -141,104 +145,125 @@ export const SystemSettings: React.FC = () => {
}

return (
<Card>
<CardHeader>
<CardTitle>System Configuration</CardTitle>
<CardDescription>
Core system settings and behavior
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<>
<Card>
<CardHeader>
<CardTitle>System Configuration</CardTitle>
<CardDescription>
Core system settings and behavior
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}

{success && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>{success}</AlertDescription>
</Alert>
)}

<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="update-frequency">Polling Interval (seconds)</Label>
<Input
id="update-frequency"
type="number"
min="5"
max="300"
value={config.update_frequency}
onChange={(e) => updateConfig('update_frequency', parseInt(e.target.value))}
placeholder="5"
disabled={isSaving}
/>
<p className="text-sm text-muted-foreground">
How often to poll Plex, download clients, and SNMP (5-300 seconds, default: 5)
</p>
</div>

<div className="space-y-2">
<Label htmlFor="log-level">Log Level</Label>
<Select
value={config.log_level}
onValueChange={(value) => updateConfig('log_level', value)}
disabled={isSaving}
>
<SelectTrigger id="log-level">
<SelectValue placeholder="Select log level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DEBUG">Debug</SelectItem>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="WARNING">Warning</SelectItem>
<SelectItem value="ERROR">Error</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Logging verbosity level (default: INFO)
</p>
</div>

<div className="space-y-2 pt-4 border-t">
<Label>Logs</Label>
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleDownloadLogs}
disabled={isDownloadingLogs}
>
{isDownloadingLogs ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Download className="mr-2 h-4 w-4" />
)}
Download Logs
</Button>
</div>
<p className="text-sm text-muted-foreground">
Download application logs with sensitive data (passwords, API keys) redacted
</p>
</div>

{success && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>{success}</AlertDescription>
</Alert>
)}

<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="update-frequency">Polling Interval (seconds)</Label>
<Input
id="update-frequency"
type="number"
min="5"
max="300"
value={config.update_frequency}
onChange={(e) => updateConfig('update_frequency', parseInt(e.target.value))}
placeholder="5"
disabled={isSaving}
/>
<p className="text-sm text-muted-foreground">
How often to poll Plex, download clients, and SNMP (5-300 seconds, default: 5)
</p>
</div>

<div className="space-y-2">
<Label htmlFor="log-level">Log Level</Label>
<Select
value={config.log_level}
onValueChange={(value) => updateConfig('log_level', value)}
<div className="flex gap-2 pt-4">
<Button
ref={saveButtonRef}
onClick={handleSave}
disabled={isSaving}
className={isDirty ? 'ring-2 ring-orange-500 ring-offset-2' : ''}
>
<SelectTrigger id="log-level">
<SelectValue placeholder="Select log level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DEBUG">Debug</SelectItem>
<SelectItem value="INFO">Info</SelectItem>
<SelectItem value="WARNING">Warning</SelectItem>
<SelectItem value="ERROR">Error</SelectItem>
<SelectItem value="CRITICAL">Critical</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Logging verbosity level (default: INFO)
</p>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</div>
</CardContent>
</Card>

<div className="space-y-2 pt-4 border-t">
<Label>Logs</Label>
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleDownloadLogs}
disabled={isDownloadingLogs}
>
{isDownloadingLogs ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Download className="mr-2 h-4 w-4" />
)}
Download Logs
</Button>
</div>
<p className="text-sm text-muted-foreground">
Download application logs with sensitive data (passwords, API keys) redacted
</p>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Info className="h-5 w-5" />
About
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
<span className="text-muted-foreground">Version</span>
<span>{versionDisplay}</span>
<span className="text-muted-foreground">Commit</span>
<span className="font-mono">{__APP_COMMIT__}</span>
<span className="text-muted-foreground">Branch</span>
<span>{__APP_BRANCH__}</span>
</div>

</div>

<div className="flex gap-2 pt-4">
<Button
ref={saveButtonRef}
onClick={handleSave}
disabled={isSaving}
className={isDirty ? 'ring-2 ring-orange-500 ring-offset-2' : ''}
>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</>
);
};
3 changes: 3 additions & 0 deletions frontend/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare const __APP_VERSION__: string;
declare const __APP_COMMIT__: string;
declare const __APP_BRANCH__: string;
Loading