From 40e3f73aafa234a9e3003b0f023f2ede7e7c710a Mon Sep 17 00:00:00 2001
From: Claude
Date: Sun, 1 Mar 2026 03:05:18 +0000
Subject: [PATCH] Add contact form DB storage and hidden staff inbox
- contact.php now inserts submissions into MySQL via PDO prepared
statements; raw values stored (htmlspecialchars moved to output only)
- www/includes/db.php: shared PDO helper with auto-migration that adds
the is_read column to existing deployments without a full DB reset
- docker/mysql/init.sql: added is_read TINYINT column to contacts table
for fresh deploys
- www/pages/admin-inbox.php: self-contained staff inbox at /staff-portal
with session-based password login, per-message mark-as-read, and
mark-all-read; unread count shown in browser tab title
- index.php: routes /staff-portal before public header/footer so the
admin page is fully standalone
- docker-compose.yml: ADMIN_PASS env var wired to web container
Set ADMIN_PASS in .env (gitignored) before deploying.
If the DB volume already exists, the auto-migration in db.php will
add the is_read column automatically on first request.
https://claude.ai/code/session_015wpwmheufcxkBuXivrSHhd
---
docker-compose.yml | 1 +
docker/mysql/init.sql | 1 +
www/includes/db.php | 35 ++++++
www/index.php | 6 +
www/pages/admin-inbox.php | 239 ++++++++++++++++++++++++++++++++++++++
www/pages/contact.php | 32 +++--
6 files changed, 304 insertions(+), 10 deletions(-)
create mode 100644 www/includes/db.php
create mode 100644 www/pages/admin-inbox.php
diff --git a/docker-compose.yml b/docker-compose.yml
index 2c57894..4659f4b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,7 @@ services:
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
+ - ADMIN_PASS=${ADMIN_PASS}
depends_on:
- db
networks:
diff --git a/docker/mysql/init.sql b/docker/mysql/init.sql
index 970868a..ad7eda5 100644
--- a/docker/mysql/init.sql
+++ b/docker/mysql/init.sql
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS `contacts` (
`phone` VARCHAR(30),
`subject` VARCHAR(255),
`message` TEXT NOT NULL,
+ `is_read` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
diff --git a/www/includes/db.php b/www/includes/db.php
new file mode 100644
index 0000000..af90daf
--- /dev/null
+++ b/www/includes/db.php
@@ -0,0 +1,35 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ ]
+ );
+
+ // Auto-migrate: add is_read if the DB was initialised before this column existed
+ $col_exists = $pdo->query(
+ "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'contacts' AND COLUMN_NAME = 'is_read'"
+ )->fetchColumn();
+
+ if (!$col_exists) {
+ $pdo->exec("ALTER TABLE contacts ADD COLUMN is_read TINYINT(1) NOT NULL DEFAULT 0");
+ }
+
+ return $pdo;
+}
diff --git a/www/index.php b/www/index.php
index 8ef1c20..811d17a 100644
--- a/www/index.php
+++ b/www/index.php
@@ -2,6 +2,12 @@
// Simple front controller — expand routing here later
$path = trim($_GET['path'] ?? '', '/');
+// Staff inbox — self-contained, no public header/footer
+if ($path === 'staff-portal') {
+ require __DIR__ . '/pages/admin-inbox.php';
+ exit;
+}
+
// Map paths to page includes
$pages = [
'' => 'pages/home.php',
diff --git a/www/pages/admin-inbox.php b/www/pages/admin-inbox.php
new file mode 100644
index 0000000..e23455c
--- /dev/null
+++ b/www/pages/admin-inbox.php
@@ -0,0 +1,239 @@
+
+
+
+
+
+
+ Staff Portal – ALWISP
+
+
+
+
+
ALWISP Staff Portal
+
+
= htmlspecialchars($login_error) ?>
+
+
+
+
+
+ Database connection failed: ' . htmlspecialchars($e->getMessage()) . '
');
+}
+
+// Mark a single message as read
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_read'])) {
+ $db->prepare("UPDATE contacts SET is_read = 1 WHERE id = ?")->execute([(int)$_POST['mark_read']]);
+ header('Location: /staff-portal');
+ exit;
+}
+
+// Mark all as read
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mark_all_read'])) {
+ $db->exec("UPDATE contacts SET is_read = 1");
+ header('Location: /staff-portal');
+ exit;
+}
+
+// ── Fetch messages ────────────────────────────────────────────────────────
+$messages = $db->query("SELECT * FROM contacts ORDER BY created_at DESC")->fetchAll();
+$unread_count = (int)$db->query("SELECT COUNT(*) FROM contacts WHERE is_read = 0")->fetchColumn();
+
+$subject_labels = [
+ 'new-service' => 'New Service Inquiry',
+ 'support' => 'Technical Support',
+ 'billing' => 'Billing Question',
+ 'coverage' => 'Coverage Question',
+ 'other' => 'Other',
+];
+
+function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES); }
+
+?>
+
+
+
+
+
+ = $unread_count ? "($unread_count) " : '' ?>Inbox – ALWISP Staff
+
+
+
+
+
+
ALWISP Staff Portal
+
+
+
+
+
+ = $unread_count ?> unread
+ = count($messages) ?> total submission= count($messages) !== 1 ? 's' : '' ?>
+
+
+
+
+
📭
+
No messages yet. They'll show up here once someone submits the contact form.
+
+
+
+
+
+
+
+
= h($m['message']) ?>
+
+
+
+
+
+
+
+
+
diff --git a/www/pages/contact.php b/www/pages/contact.php
index 76bffe7..a864afb 100644
--- a/www/pages/contact.php
+++ b/www/pages/contact.php
@@ -1,21 +1,33 @@
prepare(
+ "INSERT INTO contacts (name, email, phone, subject, message)
+ VALUES (?, ?, ?, ?, ?)"
+ );
+ $stmt->execute([$name, $email, $phone, $subject, $message]);
+ $success = true;
+ } catch (PDOException $e) {
+ $errors[] = 'Sorry, we could not save your message right now. Please try again.';
+ }
}
}
?>