commit 9ceccc1dbd17d0a0b7cc5f98caa7d1adce615387 Author: Lumerel Deploy Date: Wed Feb 18 03:29:50 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..5adfa7c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM webdevops/php-nginx:8.3-alpine +ENV WEB_DOCUMENT_ROOT=/app +ARG CACHE_BUST=1771385390 +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/api/todos.php b/api/todos.php new file mode 100644 index 0000000..2c1fd8a --- /dev/null +++ b/api/todos.php @@ -0,0 +1,208 @@ +query("SELECT * FROM todos ORDER BY created_at DESC"); + $todos = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Convert is_completed to boolean + foreach ($todos as &$todo) { + $todo['is_completed'] = (bool) $todo['is_completed']; + } + + http_response_code(200); + echo json_encode([ + 'success' => true, + 'data' => $todos + ]); + break; + + case 'POST': + // Create new todo + $input = json_decode(file_get_contents('php://input'), true); + + if (!isset($input['title']) || trim($input['title']) === '') { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => 'Title is required' + ]); + break; + } + + $title = trim($input['title']); + $description = isset($input['description']) ? trim($input['description']) : ''; + + $stmt = $pdo->prepare("INSERT INTO todos (title, description) VALUES (?, ?)"); + $stmt->execute([$title, $description]); + + $id = $pdo->lastInsertId(); + + // Fetch the created todo + $stmt = $pdo->prepare("SELECT * FROM todos WHERE id = ?"); + $stmt->execute([$id]); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + $todo['is_completed'] = (bool) $todo['is_completed']; + + http_response_code(201); + echo json_encode([ + 'success' => true, + 'data' => $todo + ]); + break; + + case 'PUT': + // Update todo + $input = json_decode(file_get_contents('php://input'), true); + + if (!isset($input['id'])) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => 'Todo ID is required' + ]); + break; + } + + $id = $input['id']; + + // Check if todo exists + $stmt = $pdo->prepare("SELECT * FROM todos WHERE id = ?"); + $stmt->execute([$id]); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$todo) { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Todo not found' + ]); + break; + } + + // Build update query dynamically based on provided fields + $updates = []; + $params = []; + + if (isset($input['title'])) { + $updates[] = "title = ?"; + $params[] = trim($input['title']); + } + + if (isset($input['description'])) { + $updates[] = "description = ?"; + $params[] = trim($input['description']); + } + + if (isset($input['is_completed'])) { + $updates[] = "completed = ?"; + $params[] = $input['is_completed'] ? 1 : 0; + } + + if (empty($updates)) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => 'No fields to update' + ]); + break; + } + + $params[] = $id; + $sql = "UPDATE todos SET " . implode(', ', $updates) . " WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + // Fetch updated todo + $stmt = $pdo->prepare("SELECT * FROM todos WHERE id = ?"); + $stmt->execute([$id]); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + $todo['is_completed'] = (bool) $todo['is_completed']; + + http_response_code(200); + echo json_encode([ + 'success' => true, + 'data' => $todo + ]); + break; + + case 'DELETE': + // Delete todo + $input = json_decode(file_get_contents('php://input'), true); + + if (!isset($input['id'])) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => 'Todo ID is required' + ]); + break; + } + + $id = $input['id']; + + // Check if todo exists + $stmt = $pdo->prepare("SELECT * FROM todos WHERE id = ?"); + $stmt->execute([$id]); + $todo = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$todo) { + http_response_code(404); + echo json_encode([ + 'success' => false, + 'error' => 'Todo not found' + ]); + break; + } + + // Delete the todo + $stmt = $pdo->prepare("DELETE FROM todos WHERE id = ?"); + $stmt->execute([$id]); + + http_response_code(200); + echo json_encode([ + 'success' => true, + 'message' => 'Todo deleted successfully' + ]); + break; + + default: + http_response_code(405); + echo json_encode([ + 'success' => false, + 'error' => 'Method not allowed' + ]); + break; + } +} catch (PDOException $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => 'Database error: ' . $e->getMessage() + ]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => 'Server error: ' . $e->getMessage() + ]); +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cf50c2b --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "project/app", + "description": "A minimal PHP project", + "type": "project", + "require": {}, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/db.php b/db.php new file mode 100644 index 0000000..f87e6e3 --- /dev/null +++ b/db.php @@ -0,0 +1,12 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] +); diff --git a/index.php b/index.php new file mode 100644 index 0000000..3d72920 --- /dev/null +++ b/index.php @@ -0,0 +1,844 @@ + + + + + + Todo App - Manage Your Tasks + + + +
+
+

Todo App

+

Organize your tasks beautifully

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

Your Tasks

+ 0 tasks +
+ +
+ + + +
+ + +
+
+ + + + diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..8219605 --- /dev/null +++ b/migrate.php @@ -0,0 +1,29 @@ +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]); + echo "Migrated: {$name}\n"; + } +} + +echo "Migration complete.\n"; diff --git a/migrations/001_create_todos_table.php b/migrations/001_create_todos_table.php new file mode 100644 index 0000000..2ca7dea --- /dev/null +++ b/migrations/001_create_todos_table.php @@ -0,0 +1,13 @@ +exec(" + CREATE TABLE IF NOT EXISTS todos ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + is_completed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) +"); +