Sat 08 Nov 2025 13:29:54 EET # deploy_property_dashboard_sqlite_shared_hosting.sh # Complete automated deployment for shared hosting (cPanel/Apache + PHP 8.1+, SQLite3) # Assumes: SSH access, PHP with PDO_SQLITE, Composer, writable /public_html or subdomain # Single-file backend (index.php), single-file frontend (index.html with embedded JS/CSS), SQLite DB auto-created # No Docker, no Node.js, no external services; pure PHP + SQLite + vanilla JS + Chart.js CDN #!/usr/bin/env bash set -euo pipefail # === Configuration (edit once) === DOMAIN="yourdomain.com" # or subdomain e.g. dashboard.yourdomain.com INSTALL_DIR="/home/username/public_html/dashboard" # adjust to your shared hosting path DB_FILE="$INSTALL_DIR/property_dashboard.db" ADMIN_USER="admin" ADMIN_PASS="password123" # change in production JWT_SECRET="SuperSecretJWTKey2025SharedHosting" # === Create Directory Structure === mkdir -p "$INSTALL_DIR" cd "$INSTALL_DIR" # === SQLite Database with Schema and Seed Data === cat > init_db.php <<'EOF' setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec(" CREATE TABLE IF NOT EXISTS properties ( id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT NOT NULL UNIQUE, units INTEGER NOT NULL CHECK(units > 0), occupancy INTEGER DEFAULT 0 CHECK(occupancy >= 0), monthly_rent REAL NOT NULL ); CREATE TABLE IF NOT EXISTS tenants ( id INTEGER PRIMARY KEY AUTOINCREMENT, property_id INTEGER REFERENCES properties(id) ON DELETE CASCADE, name TEXT NOT NULL, email TEXT, phone TEXT, lease_start DATE NOT NULL, lease_end DATE NOT NULL, rent_due REAL NOT NULL ); CREATE TABLE IF NOT EXISTS payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id INTEGER REFERENCES tenants(id) ON DELETE CASCADE, amount REAL NOT NULL, paid_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT CHECK(status IN ('paid','pending','overdue')) DEFAULT 'pending' ); CREATE TABLE IF NOT EXISTS workorders ( id INTEGER PRIMARY KEY AUTOINCREMENT, property_id INTEGER REFERENCES properties(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT, status TEXT CHECK(status IN ('new','assigned','in_progress','completed')) DEFAULT 'new', assigned_to TEXT, cost REAL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status); CREATE INDEX IF NOT EXISTS idx_workorders_status ON workorders(status); "); $db->exec(" INSERT OR IGNORE INTO properties (address, units, occupancy, monthly_rent) VALUES ('123 Elm St', 12, 10, 1500.00), ('456 Oak Ave', 8, 8, 1800.00), ('789 Pine Rd', 20, 15, 1300.00); INSERT OR IGNORE INTO tenants (property_id, name, email, lease_start, lease_end, rent_due) VALUES (1, 'Alice Johnson', 'alice@example.com', '2025-01-01', '2026-01-01', 1500.00), (1, 'Bob Smith', 'bob@example.com', '2025-02-01', '2026-02-01', 1500.00), (2, 'Carol White', 'carol@example.com', '2024-12-01', '2025-12-01', 1800.00); "); echo "SQLite database initialized at $argv[1]"; ?> EOF # === Backend: Single File PHP API (index.php) === cat > index.php <<'EOF' setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $secret = 'SuperSecretJWTKey2025SharedHosting'; $method = $_SERVER['REQUEST_METHOD']; $path = explode('/', trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/')); array_shift($path); // remove 'api' if routed function send($data, $code = 200) { http_response_code($code); echo json_encode($data); exit; } function auth() { global $secret; $headers = getallheaders(); $token = $headers['Authorization'] ?? ''; $token = preg_replace('/^Bearer\s+/i', '', $token); if (!$token) return send(['error' => 'No token'], 401); try { return (array) JWT::decode($token, $secret); } catch (Exception $e) { return send(['error' => 'Invalid token'], 401); } } // === Embedded JWT Library (pure PHP, no Composer) === if (!function_exists('base64url_encode')) { function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } function base64url_decode($data) { return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT)); } } class JWT { public static function encode($payload, $key) { $header = base64url_encode(json_encode(['typ' => 'JWT', 'alg' => 'HS256'])); $payload = base64url_encode(json_encode($payload)); $signature = hash_hmac('sha256', "$header.$payload", $key, true); $signature = base64url_encode($signature); return "$header.$payload.$signature"; } public static function decode($jwt, $key) { $parts = explode('.', $jwt); if (count($parts) !== 3) throw new Exception('Invalid JWT'); $signature = base64url_decode($parts[2]); $expected = hash_hmac('sha256', "$parts[0].$parts[1]", $key, true); if (!hash_equals($expected, $signature)) throw new Exception('Invalid signature'); return json_decode(base64url_decode($parts[1])); } } // === Routes === if ($path[0] === 'login' && $method === 'POST') { $input = json_decode(file_get_contents('php://input'), true); if ($input['username'] === 'admin' && $input['password'] === 'password123') { $token = JWT::encode(['username' => 'admin', 'exp' => time() + 28800], $secret); send(['token' => $token]); } send(['error' => 'Invalid credentials'], 401); } $user = auth(); // Dashboard Summary if ($method === 'GET' && empty($path[0])) { $stmt = $db->query("SELECT COUNT(*) as total, SUM(units) as total_units, SUM(occupancy) as occupied FROM properties"); $prop = $stmt->fetch(PDO::FETCH_ASSOC); $stmt = $db->query("SELECT SUM(amount) as revenue FROM payments WHERE status = 'paid' AND paid_at >= date('now', '-1 month')"); $rev = $stmt->fetch(PDO::FETCH_ASSOC); $stmt = $db->query("SELECT COUNT(*) as pending FROM workorders WHERE status != 'completed'"); $wo = $stmt->fetch(PDO::FETCH_ASSOC); send([ 'totalProperties' => (int)$prop['total'], 'occupancyRate' => $prop['total_units'] ? round(($prop['occupied'] / $prop['total_units']) * 100, 1) : 0, 'monthlyRevenue' => round($rev['revenue'] ?? 0, 2), 'pendingWorkorders' => (int)$wo['pending'] ]); } // Properties CRUD if ($path[0] === 'properties') { if ($method === 'GET') { $stmt = $db->query("SELECT * FROM properties"); send($stmt->fetchAll(PDO::FETCH_ASSOC)); } if ($method === 'POST') { $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO properties (address, units, occupancy, monthly_rent) VALUES (?, ?, ?, ?)"); $stmt->execute([$data['address'], $data['units'], $data['occupancy'] ?? 0, $data['monthly_rent']]); send(['id' => $db->lastInsertId()]); } } // Tenants CRUD if ($path[0] === 'tenants') { if ($method === 'GET') { $stmt = $db->query("SELECT t.*, p.address FROM tenants t JOIN properties p ON t.property_id = p.id"); send($stmt->fetchAll(PDO::FETCH_ASSOC)); } if ($method === 'POST') { $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO tenants (property_id, name, email, phone, lease_start, lease_end, rent_due) VALUES (?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([$data['property_id'], $data['name'], $data['email'], $data['phone'], $data['lease_start'], $data['lease_end'], $data['rent_due']]); $db->prepare("UPDATE properties SET occupancy = occupancy + 1 WHERE id = ?")->execute([$data['property_id']]); send(['id' => $db->lastInsertId()]); } } // Payments if ($path[0] === 'payments' && $method === 'POST') { $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO payments (tenant_id, amount, status) VALUES (?, ?, 'paid')"); $stmt->execute([$data['tenant_id'], $data['amount']]); send(['id' => $db->lastInsertId()]); } // Workorders CRUD if ($path[0] === 'workorders') { if ($method === 'GET') { $stmt = $db->query("SELECT w.*, p.address FROM workorders w JOIN properties p ON w.property_id = p.id ORDER BY created_at DESC"); send($stmt->fetchAll(PDO::FETCH_ASSOC)); } if ($method === 'POST') { $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO workorders (property_id, title, description, status, assigned_to, cost) VALUES (?, ?, ?, ?, ?, ?)"); $stmt->execute([$data['property_id'], $data['title'], $data['description'], $data['status'] ?? 'new', $data['assigned_to'], $data['cost'] ?? 0]); send(['id' => $db->lastInsertId()]); } if ($method === 'PATCH' && isset($path[1])) { $data = json_decode(file_get_contents('php://input'), true); $id = (int)$path[1]; $fields = []; $values = []; foreach (['status', 'assigned_to', 'cost'] as $f) { if (isset($data[$f])) { $fields[] = "$f = ?"; $values[] = $data[$f]; } } if (empty($fields)) send(['error' => 'No fields'], 400); $values[] = $id; $sql = "UPDATE workorders SET " . implode(', ', $fields) . " WHERE id = ?"; $db->prepare($sql)->execute($values); $stmt = $db->prepare("SELECT * FROM workorders WHERE id = ?"); $stmt->execute([$id]); send($stmt->fetch(PDO::FETCH_ASSOC)); } } send(['error' => 'Not found'], 404); ?> EOF # === Frontend: Single HTML File with Embedded JS/CSS + Chart.js CDN === cat > index.html <<'EOF'