mirror of
https://github.com/LukeHagar/pypistats.dev.git
synced 2025-12-06 12:47:48 +00:00
Update docker-compose.yml to standardize environment variable names for Postgres and Redis services. Remove README_DOCKER.md and admin dashboard components, streamlining the project structure and focusing on core functionalities.
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
### Running locally with Docker
|
||||
|
||||
Prerequisites: Docker and Docker Compose.
|
||||
|
||||
1. Build and start the full stack (Postgres, Redis, Web):
|
||||
|
||||
```
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
2. Configure BigQuery credentials via environment variables (e.g., export `GOOGLE_PROJECT_ID` and `GOOGLE_APPLICATION_CREDENTIALS_JSON`). For local compose, you can add them under the `web.environment` section in `docker-compose.yml`.
|
||||
|
||||
3. The app runs on `http://localhost:3000`.
|
||||
|
||||
Environment variables of interest:
|
||||
- `DATABASE_URL`: Postgres connection string.
|
||||
- `REDIS_URL`: Redis URL.
|
||||
- `ENABLE_CRON`: Set to `true` to run the daily ETL.
|
||||
- `CRON_SCHEDULE`: Cron string (default 2 AM UTC daily).
|
||||
- `GOOGLE_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS_JSON` or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery.
|
||||
|
||||
The container entrypoint waits for Postgres, applies Prisma migrations, then starts the app.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
environment:
|
||||
POSTGRES_DB: pypistats
|
||||
POSTGRES_USER: pypistats
|
||||
POSTGRES_PASSWORD: ${SERVICE_PASSWORD_POSTGRES}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
@@ -18,12 +18,12 @@ services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
environment:
|
||||
SERVICE_PASSWORD_REDIS: ${SERVICE_PASSWORD_REDIS}
|
||||
command: [ "redis-server", "--requirepass", "${SERVICE_PASSWORD_REDIS}" ]
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "redis-cli -a $SERVICE_PASSWORD_REDIS ping" ]
|
||||
test: [ "CMD-SHELL", "redis-cli -a $REDIS_PASSWORD ping" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
DATABASE_URL: postgresql://pypistats:${SERVICE_PASSWORD_POSTGRES}@db:5432/pypistats?schema=public
|
||||
DATABASE_URL: postgresql://pypistats:${POSTGRES_PASSWORD}@db:5432/pypistats?schema=public
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -41,13 +41,10 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
SERVICE_PASSWORD_POSTGRES: ${SERVICE_PASSWORD_POSTGRES}
|
||||
SERVICE_PASSWORD_REDIS: ${SERVICE_PASSWORD_REDIS}
|
||||
DATABASE_URL: postgresql://pypistats:${SERVICE_PASSWORD_POSTGRES}@db:5432/pypistats?schema=public
|
||||
REDIS_URL: redis://:${SERVICE_PASSWORD_REDIS}@redis:6379
|
||||
GOOGLE_PROJECT_ID: ${SERVICE_GOOGLE_PROJECT_ID}
|
||||
GOOGLE_APPLICATION_CREDENTIALS_JSON: ${SERVICE_GOOGLE_CREDENTIALS}
|
||||
SERVICE_FQDN_WEB_3000: "" # Coolify will generate the public URL
|
||||
SERVICE_PASSWORD_POSTGRES: ${POSTGRES_PASSWORD}
|
||||
SERVICE_PASSWORD_REDIS: ${REDIS_PASSWORD}
|
||||
DATABASE_URL: postgresql://pypistats:${POSTGRES_PASSWORD}@db:5432/pypistats?schema=public
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command: [ "sh", "-c", "bun run prisma migrate deploy && bun run build/index.js" ]
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let processing = $state(false);
|
||||
let cacheClearing = $state(false);
|
||||
let results: any = $state(null);
|
||||
let error: string | null = $state(null);
|
||||
let cacheInfo: any = $state(null);
|
||||
let cronStatus: any = $state(null);
|
||||
let date = $state('');
|
||||
let purge = $state(true);
|
||||
let packageName = $state('');
|
||||
let searchQuery = $state('');
|
||||
|
||||
// Get yesterday's date as default
|
||||
onMount(() => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
date = d.toISOString().split('T')[0];
|
||||
});
|
||||
|
||||
async function processData() {
|
||||
processing = true;
|
||||
error = null;
|
||||
results = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/process-data', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ date, purge })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
results = data.results;
|
||||
} else {
|
||||
error = data.message || 'Data processing failed';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Network error';
|
||||
} finally {
|
||||
processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCacheInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/cache');
|
||||
cacheInfo = await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to get cache info:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllCache() {
|
||||
cacheClearing = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/cache', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'clear' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await getCacheInfo();
|
||||
} else {
|
||||
error = data.message || 'Failed to clear cache';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Network error';
|
||||
} finally {
|
||||
cacheClearing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function invalidatePackageCache() {
|
||||
if (!packageName.trim()) {
|
||||
error = 'Package name is required';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/cache', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'invalidate-package',
|
||||
packageName: packageName.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await getCacheInfo();
|
||||
} else {
|
||||
error = data.message || 'Failed to invalidate package cache';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Network error';
|
||||
}
|
||||
}
|
||||
|
||||
async function invalidateSearchCache() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/cache', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action: 'invalidate-search' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
await getCacheInfo();
|
||||
} else {
|
||||
error = data.message || 'Failed to invalidate search cache';
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Network error';
|
||||
}
|
||||
}
|
||||
|
||||
async function getCronStatus() { cronStatus = null; }
|
||||
|
||||
async function runCronNow() {}
|
||||
|
||||
// Load cache info and cron status on mount
|
||||
onMount(() => {
|
||||
getCacheInfo();
|
||||
getCronStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Admin Dashboard - PyPI Stats</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Not Found</h1>
|
||||
<p class="mt-2 text-gray-600">The admin dashboard has been removed.</p>
|
||||
<p class="mt-4"><a href="/" class="text-blue-600 hover:text-blue-800">Return to homepage</a></p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="mb-6 bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Error</h3>
|
||||
<div class="mt-2 text-sm text-red-700">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Data Processing Section -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Data Processing</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="date" class="block text-sm font-medium text-gray-700">Processing Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
bind:value={date}
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="purge"
|
||||
bind:checked={purge}
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label for="purge" class="ml-2 block text-sm text-gray-900">
|
||||
Purge old data (keep only 180 days)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
onclick={processData}
|
||||
disabled={processing}
|
||||
class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? 'Processing...' : 'Process Data'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={runCronNow}
|
||||
disabled={processing}
|
||||
class="w-full bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{processing ? 'Processing...' : 'Run Cron Now'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if results}
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-3">Results</h3>
|
||||
<div class="bg-gray-50 rounded-md p-4">
|
||||
<pre class="text-sm text-gray-700 overflow-auto">{JSON.stringify(results, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Cron removed -->
|
||||
|
||||
<!-- Cache Management Section -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Cache Management</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="packageName" class="block text-sm font-medium text-gray-700">Package Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="packageName"
|
||||
bind:value={packageName}
|
||||
placeholder="e.g., numpy"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
onclick={invalidatePackageCache}
|
||||
disabled={!packageName.trim()}
|
||||
class="bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Invalidate Package Cache
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={invalidateSearchCache}
|
||||
class="bg-orange-600 text-white px-4 py-2 rounded-md hover:bg-orange-700"
|
||||
>
|
||||
Invalidate Search Cache
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={clearAllCache}
|
||||
disabled={cacheClearing}
|
||||
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{cacheClearing ? 'Clearing...' : 'Clear All Cache'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if cacheInfo}
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-3">Cache Information</h3>
|
||||
<div class="bg-gray-50 rounded-md p-4">
|
||||
<pre class="text-sm text-gray-700 overflow-auto">{JSON.stringify(cacheInfo, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Environment Information -->
|
||||
<div class="mt-8 bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Environment Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700">Database</h3>
|
||||
<p class="text-sm text-gray-900">
|
||||
{typeof process !== 'undefined' && process.env.DATABASE_URL ? 'Configured' : 'Not configured'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700">Google Cloud</h3>
|
||||
<p class="text-sm text-gray-900">
|
||||
{typeof process !== 'undefined' && process.env.GOOGLE_PROJECT_ID ? 'Configured' : 'Not configured'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700">Redis</h3>
|
||||
<p class="text-sm text-gray-900">
|
||||
{typeof process !== 'undefined' && process.env.REDIS_URL ? 'Configured' : 'Not configured'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700">Environment</h3>
|
||||
<p class="text-sm text-gray-900">
|
||||
{typeof process !== 'undefined' ? process.env.NODE_ENV || 'development' : 'development'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user