commit b8ddf08f495d3e9ae868bd26982b3df6aeca7cfe Author: Lumerel Deploy Date: Sun Apr 5 00:50:02 2026 +0000 Deploy from Lumerel diff --git a/.docker/99-migrate.sh b/.docker/99-migrate.sh new file mode 100644 index 0000000..66b83d4 --- /dev/null +++ b/.docker/99-migrate.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# IMPORTANT: This script is sourced (not executed) by the webdevops entrypoint. +# Never use "exit" — it would kill the parent entrypoint and prevent supervisord from starting. +# Use "return" instead to leave this script without affecting the parent. + +echo ">>> Waiting for database connection..." +echo ">>> DB_HOST=${DB_HOST:-NOT SET}" +echo ">>> DB_PORT=${DB_PORT:-NOT SET}" +echo ">>> DB_DATABASE=${DB_DATABASE:-NOT SET}" +echo ">>> DB_USERNAME=${DB_USERNAME:-NOT SET}" +echo ">>> DB_PASSWORD is $([ -n "$DB_PASSWORD" ] && echo 'set' || echo 'NOT SET')" + +if [ -z "$DB_HOST" ] || [ -z "$DB_USERNAME" ]; then + echo ">>> ERROR: DB_HOST or DB_USERNAME not set. Skipping migrations." + return 0 2>/dev/null || true +fi + +_migrate_max_retries=30 +_migrate_count=0 +_migrate_done=false + +while [ $_migrate_count -lt $_migrate_max_retries ]; do + if php -r ' + try { + $host = getenv("DB_HOST"); + $port = getenv("DB_PORT") ?: "3306"; + $user = getenv("DB_USERNAME"); + $pass = getenv("DB_PASSWORD"); + new PDO("mysql:host=$host;port=$port", $user, $pass, [PDO::ATTR_TIMEOUT => 3]); + exit(0); + } catch (Throwable $e) { + fwrite(STDERR, "PDO error: " . $e->getMessage() . "\n"); + exit(1); + } + ' 2>&1; then + echo ">>> Database is reachable. Running migrations..." + if php /app/migrate.php; then + echo ">>> Migrations completed successfully." + else + echo ">>> WARNING: migrate.php exited with code $?" + fi + _migrate_done=true + break + fi + + _migrate_count=$((_migrate_count + 1)) + echo ">>> Waiting for database... attempt $_migrate_count/$_migrate_max_retries" + sleep 2 +done + +if [ "$_migrate_done" = false ]; then + echo ">>> WARNING: Could not connect to database after $_migrate_max_retries attempts. Skipping migrations." +fi \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..17896fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +Dockerfile +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b04720 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app +ARG CACHE_BUST=1775350202 +COPY . /app +RUN echo "index index.php index.html index.htm;" > /opt/docker/etc/nginx/vhost.common.d/01-index.conf \ + && echo "add_header Cache-Control 'no-cache, no-store, must-revalidate';" > /opt/docker/etc/nginx/vhost.common.d/02-no-cache.conf +RUN set -e; if [ -f /app/composer.json ]; then \ + echo ">>> composer.json found, installing dependencies..."; \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \ + cd /app && composer install --no-dev --no-interaction --optimize-autoloader; \ + ls -la /app/vendor/autoload.php; \ + else \ + echo ">>> No composer.json found, skipping composer install"; \ + fi +RUN set -e; if [ -f /app/package.json ]; then \ + echo ">>> package.json found, installing node dependencies..."; \ + apk add --no-cache nodejs npm; \ + cd /app && npm install --production; \ + else \ + echo ">>> No package.json found, skipping npm install"; \ + fi +COPY --chmod=755 .docker/99-migrate.sh /opt/docker/provision/entrypoint.d/99-migrate.sh +RUN chown -R application:application /app \ No newline at end of file diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..075e546 --- /dev/null +++ b/admin.php @@ -0,0 +1,672 @@ +query("SELECT * FROM training_items ORDER BY sort_order ASC")->fetchAll() + : []; +?> + + + + + +Pawgress Admin ⚙️ + + + + + + +
+ +

Pawgress Admin

+

Manage your daily training checklist

+ + + +
+ + + +
+ +
+ + + +
+ +
+
+

Training Items

+

item • Drag to reorder

+
+ +
+ + +
+
🐕
+

No Training Items Yet

+

Click "Add Item" above to create your first training task.

+
+ +
+ $item): ?> +
+
+
+
+
+ +
+ +
+
+ + +
+
+ +
+ + +
+ + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/api.php b/api.php new file mode 100644 index 0000000..c2c9aba --- /dev/null +++ b/api.php @@ -0,0 +1,76 @@ + 'Unauthorized']); + exit; +} + +$body = json_decode(file_get_contents('php://input'), true); +$action = $body['action'] ?? ''; + +try { + switch ($action) { + + // ── CREATE ──────────────────────────────────────────────────── + case 'create': { + $title = trim($body['title'] ?? ''); + $desc = trim($body['description'] ?? ''); + if (!$title) { + echo json_encode(['error' => 'Title is required']); exit; + } + $maxOrder = $pdo->query(\"SELECT COALESCE(MAX(sort_order),0) FROM training_items\")->fetchColumn(); + $stmt = $pdo->prepare(\"INSERT INTO training_items (title, description, sort_order) VALUES (?, ?, ?)\"); + $stmt->execute([$title, $desc ?: null, (int)$maxOrder + 1]); + echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); + break; + } + + // ── UPDATE ──────────────────────────────────────────────────── + case 'update': { + $id = (int)($body['id'] ?? 0); + $title = trim($body['title'] ?? ''); + $desc = trim($body['description'] ?? ''); + if (!$id || !$title) { + echo json_encode(['error' => 'ID and title are required']); exit; + } + $stmt = $pdo->prepare(\"UPDATE training_items SET title=?, description=?, updated_at=NOW() WHERE id=?\"); + $stmt->execute([$title, $desc ?: null, $id]); + echo json_encode(['success' => true]); + break; + } + + // ── DELETE ──────────────────────────────────────────────────── + case 'delete': { + $id = (int)($body['id'] ?? 0); + if (!$id) { echo json_encode(['error' => 'ID required']); exit; } + $pdo->prepare(\"DELETE FROM training_items WHERE id=?\")->execute([$id]); + echo json_encode(['success' => true]); + break; + } + + // ── REORDER ─────────────────────────────────────────────────── + case 'reorder': { + $ids = $body['ids'] ?? []; + if (!is_array($ids)) { echo json_encode(['error' => 'IDs must be an array']); exit; } + $stmt = $pdo->prepare(\"UPDATE training_items SET sort_order=? WHERE id=?\"); + foreach ($ids as $i => $id) { + $stmt->execute([$i + 1, (int)$id]); + } + echo json_encode(['success' => true]); + break; + } + + default: + echo json_encode(['error' => 'Unknown action']); + } + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Database error: ' . $e->getMessage()]); +} \ No newline at end of file diff --git a/db.php b/db.php new file mode 100644 index 0000000..bac204d --- /dev/null +++ b/db.php @@ -0,0 +1,13 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); +} catch (PDOException $e) { + http_response_code(500); + die(json_encode(['error' => 'Database connection failed.'])); +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..9e8b048 --- /dev/null +++ b/index.php @@ -0,0 +1,510 @@ +query("SELECT * FROM training_items ORDER BY sort_order ASC")->fetchAll(); +$today = date('Y-m-d'); +?> + + + + + +Pawgress 🐾 Daily Training + + + + + + +
+ +

Pawgress

+

• Daily Training

+
+ +
+ + +
+
+ + + + + 🐶 +
+
+
Today's Progress
+
0 /
+
+
+
+
Let's get training! 💪
+
+
+ + +
+

🎉 Amazing Work!

+

You crushed every training task today. Your pup is so proud!

+
+ + + +
+
🐕
+

No Training Items Yet

+

Head to the admin panel to add your first training tasks.

+ Go to Admin → +
+ +
Training Checklist
+
+ +
+
+ + + +
+
+
+ +
+ +
+
+ +
+ + + + +
+ + + + + + \ No newline at end of file diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..9043fb7 --- /dev/null +++ b/migrate.php @@ -0,0 +1,24 @@ +exec("CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL UNIQUE, + ran_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"); + +// Get already-run migrations +$ran = $pdo->query("SELECT migration FROM migrations")->fetchAll(PDO::FETCH_COLUMN); + +// Run pending migrations +$files = glob(__DIR__ . '/migrations/*.php'); +sort($files); + +foreach ($files as $file) { + $name = basename($file); + if (!in_array($name, $ran)) { + require $file; + $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)")->execute([$name]); + } +} diff --git a/migrations/001_create_training_items_table.php b/migrations/001_create_training_items_table.php new file mode 100644 index 0000000..64d0059 --- /dev/null +++ b/migrations/001_create_training_items_table.php @@ -0,0 +1,61 @@ +exec(" + CREATE TABLE IF NOT EXISTS training_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) +"); + +// Seed default items if table is empty +$count = $pdo->query("SELECT COUNT(*) FROM training_items")->fetchColumn(); +if ($count == 0) { + $items = [ + [ + 'Minute 0–3: Calm training', + "Sit down somewhere boring.\n\n +• Ignore her completely\n +• The second she settles even a little (lays down, sighs, pauses) → quietly say “yes” and give a treat\n +• No excitement, no talking\n\n +Goal: teach her that calm = reward", + 1 + ], + [ + 'Minute 3–6: Leash pressure game', + "Inside the house.\n\n +• Put leash on\n +• Apply gentle pressure (not a yank, just steady)\n +• The moment she moves toward you → “yes” + treat\n\n +Goal: pressure means move back, not fight it", + 2 + ], + [ + 'Minute 6–8: Micro walk practice', + "Inside or just outside your door.\n\n +• Take a few steps\n +• If leash tightens → stop immediately\n +• When she gives slack → move again\n\n +This is practice, not exercise", + 3 + ], + [ + 'Minute 8–10: Separation practice', + "Step away briefly.\n\n +• Step out of sight\n +• Count to 3\n +• Come back before she reacts\n +• Stay calm, no excitement when returning\n\n +Repeat 5–10 times\n\n +Goal: leaving and returning is no big deal", + 4 + ], + ]; + + $stmt = $pdo->prepare("INSERT INTO training_items (title, description, sort_order) VALUES (?, ?, ?)"); + foreach ($items as $item) { + $stmt->execute($item); + } +} \ No newline at end of file