Merge pull request 'Privacy Policy, Terms & animated landing page' (#83) from feat/privacy-terms-landing into master

Reviewed-on: #83
Reviewed-by: rafaga21 <rafaelminaya20@hotmail.com>
This commit was merged in pull request #83.
This commit is contained in:
2026-06-13 21:34:46 -04:00
9 changed files with 815 additions and 1 deletions

View File

@@ -0,0 +1,2 @@
INSERT IGNORE INTO app_config (`key`, `value`) VALUES ('privacy_policy', 'PRIVACY POLICY\nLast updated: June 2026\n\n1. DATA CONTROLLER\nServerManager (hereinafter "the Platform") is a server management tool. The data controller is the organization or individual who deploys and operates the Platform.\n\n2. DATA WE COLLECT\nWe collect and process the following data:\n\na) Account Information: username, email address, password (hashed with Argon2id), and role assignment.\n\nb) Server Credentials: SSH hostnames, IP addresses, ports, usernames, and private keys or passwords. All credentials are encrypted at rest using AES-256-CBC with a master key stored outside the database.\n\nc) Monitoring Data: CPU usage, RAM usage, disk usage, network traffic, process lists, and system uptime collected from connected servers.\n\nd) Usage Logs: Audit logs recording actions performed on the Platform (login, server access, configuration changes, command execution), including IP address, timestamp, and action details.\n\ne) Session Data: Session identifiers stored in the sessions table to maintain authentication state.\n\nf) Device Tokens: FCM (Firebase Cloud Messaging) tokens for push notifications on mobile devices.\n\n3. LEGAL BASIS FOR PROCESSING\nData is processed based on:\n- Legitimate interest in managing and monitoring IT infrastructure\n- Contractual necessity to provide the server management service\n- Legal obligations for audit and security logging\n\n4. DATA STORAGE AND SECURITY\n- Passwords: hashed using Argon2id\n- SSH credentials: encrypted with AES-256-CBC\n- Sessions: stored server-side in the database with HTTP-only cookies and SameSite flags\n- Communications: HTTPS encryption is enforced\n- Audit logs: immutable records of all sensitive operations\n\n5. DATA RETENTION\n- Account data: retained while the account is active and up to 12 months after deletion\n- Audit logs: retained for a minimum of 12 months\n- Monitoring data: retained according to configured thresholds\n- Session data: deleted upon logout or expiry\n\n6. YOUR RIGHTS\nDepending on applicable law, you may have the right to:\n- Access your personal data\n- Rectify inaccurate data\n- Delete your account and associated data\n- Restrict or object to processing\n- Data portability\n\nTo exercise these rights, contact your Platform administrator.\n\n7. DATA SHARING\nWe do not sell personal data. Data may be shared with:\n- Third-party services explicitly configured by the administrator (e.g., Firebase for push notifications)\n- Law enforcement only when required by applicable law\n\n8. COOKIES AND TRACKING\nThe Platform uses session cookies for authentication. No tracking cookies or third-party analytics are used.\n\n9. CHANGES TO THIS POLICY\nUsers will be notified of material changes via the Platform notification system.\n\n10. CONTACT\nContact your Platform administrator for privacy-related inquiries.');
INSERT IGNORE INTO app_config (`key`, `value`) VALUES ('terms_conditions', 'TERMS AND CONDITIONS\nLast updated: June 2026\n\n1. ACCEPTANCE OF TERMS\nBy accessing or using ServerManager (hereinafter "the Platform"), you agree to be bound by these Terms and Conditions. If you do not agree, do not use the Platform.\n\n2. DESCRIPTION OF SERVICE\nThe Platform provides web-based remote server management including but not limited to:\n- SSH terminal access\n- System monitoring (CPU, RAM, disk, network)\n- Service management (start, stop, restart)\n- Nginx configuration management\n- SSL certificate management\n- Database management\n- Process management\n- System user management\n- Team collaboration\n- Mobile device notifications\n\n3. USER ACCOUNTS\n3.1 Account Creation: Users must provide accurate information when creating an account.\n3.2 Account Security: Users are responsible for maintaining the confidentiality of their credentials and for all activities under their account.\n3.3 Role-Based Access: Access is granted based on assigned roles (Operator, Admin, Super Admin). Privilege escalation is prohibited.\n\n4. ACCEPTABLE USE\nYou agree not to:\n- Use the Platform for any unlawful purpose\n- Attempt to access servers or data for which you lack authorization\n- Share your account credentials with unauthorized persons\n- Use the Platform to disrupt, damage, or compromise connected systems\n- Circumvent access controls, authentication mechanisms, or audit logging\n- Use automated tools to scrape, overload, or abuse the Platform\n\n5. INTELLECTUAL PROPERTY\nThe Platform software and its code are the property of the respective licensors. No ownership rights are transferred to users.\n\n6. LIMITATION OF LIABILITY\nThe Platform is provided "as is" without warranty of any kind. The operators shall not be liable for:\n- Damages resulting from server misconfiguration or command execution\n- Data loss resulting from system management actions\n- Unauthorized access due to compromised user credentials\n- Service interruptions or downtime\n- Indirect, incidental, or consequential damages\n\n7. INDEMNIFICATION\nUsers agree to indemnify and hold harmless the Platform operators from claims arising from their use of the service or violation of these terms.\n\n8. TERMINATION\nThe Platform administrator may suspend or terminate access:\n- For violation of these terms\n- At their sole discretion without prior notice\n- Upon user request\n\nUpon termination, access to the Platform and connected servers will be revoked.\n\n9. MODIFICATIONS\nThese terms may be updated at any time. Continued use after changes constitutes acceptance of the new terms. Users will be notified of material changes.\n\n10. GOVERNING LAW\nThese terms are governed by the laws of the jurisdiction where the Platform is deployed and operated.\n\n11. SEVERABILITY\nIf any provision of these terms is found to be unenforceable, the remaining provisions will remain in full force and effect.\n\n12. CONTACT\nFor questions regarding these terms, contact your Platform administrator.');

View File

@@ -17,13 +17,16 @@ use ServerManager\Middleware\RoleMiddleware;
$router = App::getInstance()->getRouter();
$router->get('/', [DashboardController::class, 'index'], [AuthMiddleware::class]);
$router->get('/', [DashboardController::class, 'landing']);
$router->get('/dashboard', [DashboardController::class, 'index'], [AuthMiddleware::class]);
$router->get('/dashboard/stats', [DashboardController::class, 'stats'], [AuthMiddleware::class]);
$router->get('/dashboard/refresh-metrics', [DashboardController::class, 'refreshMetrics'], [AuthMiddleware::class]);
$router->get('/dashboard/chart-data', [DashboardController::class, 'chartData'], [AuthMiddleware::class]);
$router->get('/notifications', [DashboardController::class, 'notifications'], [AuthMiddleware::class]);
$router->get('/privacy', [DashboardController::class, 'privacy']);
$router->get('/terms', [DashboardController::class, 'terms']);
$router->get('/login', [AuthController::class, 'loginForm']);
$router->post('/login', [AuthController::class, 'login'], [CSRFMiddleware::class, RateLimitMiddleware::class]);
$router->get('/register', [AuthController::class, 'registerForm']);
@@ -99,6 +102,9 @@ $router->group(['prefix' => 'admin', 'middleware' => [AuthMiddleware::class, Rol
$router->post('/app-version', [AdminController::class, 'appVersion'], [CSRFMiddleware::class]);
$router->get('/notifications', [AdminController::class, 'notifications']);
$router->post('/notifications/send', [AdminController::class, 'sendNotification'], [CSRFMiddleware::class]);
$router->get('/policies', [AdminController::class, 'policies']);
$router->post('/policies', [AdminController::class, 'savePolicies'], [CSRFMiddleware::class]);
});
$router->get('/api/status', [ApiController::class, 'status']);

View File

@@ -19,11 +19,14 @@ class AdminController
private User $userModel;
private AuditService $auditService;
private Database $db;
public function __construct()
{
$this->view = App::getInstance()->getView();
$this->userModel = new User();
$this->auditService = new AuditService();
$this->db = Database::getInstance();
}
private function requireSuperAdmin(): void
@@ -337,4 +340,39 @@ class AdminController
$this->view->json(['success' => true, 'message' => 'Notification sent successfully.' . $pushResult]);
}
public function policies(): void
{
$this->requireSuperAdmin();
$configs = $this->db->fetchAll('SELECT `key`, `value` FROM app_config WHERE `key` IN (?, ?)', ['privacy_policy', 'terms_conditions']);
$values = [];
foreach ($configs as $row) {
$values[$row['key']] = $row['value'];
}
$this->view->display('admin.policies', [
'title' => 'Legal Policies - ServerManager',
'privacy' => $values['privacy_policy'] ?? '',
'terms' => $values['terms_conditions'] ?? '',
]);
}
public function savePolicies(): void
{
$this->requireSuperAdmin();
if (isset($_POST['privacy_policy'])) {
$this->db->update('app_config', ['value' => $_POST['privacy_policy']], '`key` = ?', ['privacy_policy']);
}
if (isset($_POST['terms_conditions'])) {
$this->db->update('app_config', ['value' => $_POST['terms_conditions']], '`key` = ?', ['terms_conditions']);
}
$this->auditService->log('policies_updated', 'config', null, ['details' => 'Legal policies updated']);
Session::setFlash('success', 'Policies saved successfully.');
$this->view->redirect('/admin/policies');
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ServerManager\Controllers;
use ServerManager\Core\App;
use ServerManager\Core\Database;
use ServerManager\Core\Session;
use ServerManager\Models\Server;
use ServerManager\Models\Notification;
@@ -24,6 +25,28 @@ class DashboardController
$this->auditService = new AuditService();
}
public function landing(): void
{
if (Session::has('user_id')) {
$this->view->redirect('/dashboard');
}
$stats = [];
try {
$db = Database::getInstance();
$stats['servers'] = $db->fetch('SELECT COUNT(*) as count FROM servers')['count'] ?? 0;
$stats['users'] = $db->fetch('SELECT COUNT(*) as count FROM users')['count'] ?? 0;
$stats['online_servers'] = $db->fetch("SELECT COUNT(*) as count FROM servers WHERE current_status = 'online'")['count'] ?? 0;
} catch (\Throwable) {
$stats = ['servers' => 0, 'users' => 0, 'online_servers' => 0];
}
$this->view->setLayout('auth')->display('pages.landing', [
'title' => 'ServerManager - Linux Server Management Platform',
'stats' => $stats,
]);
}
public function index(): void
{
$serverModel = new Server();
@@ -82,6 +105,30 @@ class DashboardController
]);
}
public function privacy(): void
{
$db = Database::getInstance();
$row = $db->fetch('SELECT `value` FROM app_config WHERE `key` = ?', ['privacy_policy']);
$content = $row ? $row['value'] : '';
$this->view->setLayout('auth')->display('pages.privacy', [
'title' => 'Privacy Policy - ServerManager',
'content' => $content,
]);
}
public function terms(): void
{
$db = Database::getInstance();
$row = $db->fetch('SELECT `value` FROM app_config WHERE `key` = ?', ['terms_conditions']);
$content = $row ? $row['value'] : '';
$this->view->setLayout('auth')->display('pages.terms', [
'title' => 'Terms & Conditions - ServerManager',
'content' => $content,
]);
}
public function notifications(): void
{
$userId = (int) Session::get('user_id');

38
views/admin/policies.php Normal file
View File

@@ -0,0 +1,38 @@
<div class="page-header">
<h1 class="page-title">Legal Policies</h1>
<p class="page-subtitle">Manage Privacy Policy and Terms & Conditions</p>
</div>
<form method="POST" action="/admin/policies">
<?= csrf_field() ?>
<div class="dashboard-card" style="margin-bottom:1.5rem;max-width:100%">
<div class="card-header">
<h2><i class="fas fa-shield-alt"></i> Privacy Policy</h2>
</div>
<div class="card-body">
<div class="form-group">
<label for="privacy_policy">Content</label>
<textarea name="privacy_policy" id="privacy_policy" class="form-control" rows="20" style="font-family:monospace;width:100%;min-height:400px;font-size:0.9rem;line-height:1.5;padding:1rem"><?= htmlspecialchars($privacy ?? '') ?></textarea>
</div>
</div>
</div>
<div class="dashboard-card" style="margin-bottom:1.5rem;max-width:100%">
<div class="card-header">
<h2><i class="fas fa-file-contract"></i> Terms & Conditions</h2>
</div>
<div class="card-body">
<div class="form-group">
<label for="terms_conditions">Content</label>
<textarea name="terms_conditions" id="terms_conditions" class="form-control" rows="20" style="font-family:monospace;width:100%;min-height:400px;font-size:0.9rem;line-height:1.5;padding:1rem"><?= htmlspecialchars($terms ?? '') ?></textarea>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</form>

View File

@@ -95,6 +95,12 @@
<span>Notifications</span>
</a>
</li>
<li class="nav-item">
<a href="/admin/policies" class="nav-link <?= str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/admin/policies') ? 'active' : '' ?>">
<i class="fas fa-file-contract"></i>
<span>Policies</span>
</a>
</li>
<?php elseif (is_admin()): ?>
<li class="nav-divider"></li>
<li class="nav-section">Administration</li>

613
views/pages/landing.php Normal file
View File

@@ -0,0 +1,613 @@
<style>
body.auth-page {
display: block;
align-items: unset;
justify-content: unset;
min-height: 100vh;
background: var(--bg-primary);
}
body.auth-page .auth-container {
max-width: none;
width: 100%;
padding: 0;
}
.landing-page {
width: 100%;
min-height: 100vh;
overflow-x: hidden;
}
.landing-hero {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 2rem;
background: radial-gradient(ellipse at 50% 30%, rgba(99,102,241,0.08) 0%, transparent 60%),
radial-gradient(ellipse at 80% 80%, rgba(34,197,94,0.05) 0%, transparent 50%),
var(--bg-primary);
text-align: center;
overflow: hidden;
}
.landing-hero::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.15) 0%, transparent 100%),
radial-gradient(1px 1px at 40% 70%, rgba(255,255,255,0.12) 0%, transparent 100%),
radial-gradient(1px 1px at 60% 20%, rgba(255,255,255,0.15) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 60%, rgba(255,255,255,0.12) 0%, transparent 100%),
radial-gradient(1px 1px at 10% 80%, rgba(255,255,255,0.08) 0%, transparent 100%),
radial-gradient(1px 1px at 90% 10%, rgba(255,255,255,0.08) 0%, transparent 100%);
pointer-events: none;
}
.floating-elements {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.floating-elem {
position: absolute;
font-size: 1.4rem;
opacity: 0.08;
animation: floatAround 20s infinite ease-in-out;
}
.floating-elem:nth-child(1) { top: 15%; left: 10%; animation-delay: 0s; font-size: 2rem; }
.floating-elem:nth-child(2) { top: 25%; right: 12%; animation-delay: -3s; }
.floating-elem:nth-child(3) { top: 60%; left: 8%; animation-delay: -7s; font-size: 1.8rem; }
.floating-elem:nth-child(4) { top: 70%; right: 8%; animation-delay: -11s; }
.floating-elem:nth-child(5) { top: 40%; left: 5%; animation-delay: -5s; font-size: 1.2rem; }
.floating-elem:nth-child(6) { top: 50%; right: 5%; animation-delay: -9s; font-size: 1.6rem; }
.floating-elem:nth-child(7) { top: 10%; left: 50%; animation-delay: -13s; font-size: 1rem; }
.floating-elem:nth-child(8) { top: 85%; left: 50%; animation-delay: -15s; }
@keyframes floatAround {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-30px) rotate(5deg); }
50% { transform: translateY(0) rotate(0deg); }
75% { transform: translateY(20px) rotate(-3deg); }
}
.hero-icon-wrapper {
position: relative;
margin-bottom: 2rem;
animation: heroIconEnter 1s ease-out;
}
.hero-server-icon {
width: 80px;
height: 80px;
border-radius: 20px;
background: linear-gradient(135deg, rgba(99,102,241,0.2), rgba(99,102,241,0.05));
border: 1px solid rgba(99,102,241,0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 2.4rem;
color: var(--accent);
position: relative;
margin: 0 auto;
}
.hero-server-icon::before {
content: '';
position: absolute;
inset: -4px;
border-radius: 24px;
background: linear-gradient(135deg, var(--accent), var(--purple));
opacity: 0.15;
z-index: -1;
animation: glowPulse 3s infinite ease-in-out;
}
@keyframes glowPulse {
0%, 100% { opacity: 0.15; transform: scale(1); }
50% { opacity: 0.3; transform: scale(1.05); }
}
@keyframes heroIconEnter {
from { opacity: 0; transform: translateY(-30px) scale(0.8); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.hero-title {
font-size: clamp(2rem, 5vw, 3.2rem);
font-weight: 800;
line-height: 1.15;
margin-bottom: 0.75rem;
animation: fadeSlideUp 0.8s ease-out 0.2s both;
}
.hero-title .gradient-text {
background: linear-gradient(135deg, var(--accent), var(--purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1rem, 2vw, 1.2rem);
color: var(--text-muted);
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
animation: fadeSlideUp 0.8s ease-out 0.35s both;
}
.hero-cta {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
animation: fadeSlideUp 0.8s ease-out 0.5s both;
}
.hero-cta .btn {
padding: 0.85rem 2.2rem;
font-size: 1rem;
border-radius: var(--radius);
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: none;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.hero-cta .btn-primary-custom {
background: linear-gradient(135deg, var(--accent), #818cf8);
color: #fff;
box-shadow: 0 4px 20px rgba(99,102,241,0.3);
}
.hero-cta .btn-primary-custom:hover {
transform: translateY(-2px);
box-shadow: 0 6px 28px rgba(99,102,241,0.4);
}
.hero-cta .btn-secondary-custom {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.hero-cta .btn-secondary-custom:hover {
background: var(--bg-hover);
border-color: var(--accent);
transform: translateY(-2px);
}
.hero-terminal {
margin-top: 3rem;
max-width: 560px;
width: 100%;
background: rgba(0,0,0,0.4);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
animation: fadeSlideUp 0.8s ease-out 0.65s both;
}
.terminal-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 1rem;
background: rgba(255,255,255,0.03);
border-bottom: 1px solid var(--border-color);
}
.terminal-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.terminal-dot.red { background: #ff5f57; }
.terminal-dot.yellow { background: #ffbd2e; }
.terminal-dot.green { background: #28c840; }
.terminal-title {
font-size: 0.75rem;
color: var(--text-muted);
margin-left: 0.5rem;
font-family: var(--font-mono);
}
.terminal-body-landing {
padding: 1.25rem 1.25rem 1.5rem;
text-align: left;
font-family: var(--font-mono);
font-size: 0.82rem;
line-height: 1.6;
color: #c9d1d9;
}
.terminal-line-landing {
opacity: 0;
animation: terminalFadeIn 0.3s ease-out forwards;
}
.terminal-line-landing .prompt { color: var(--success); }
.terminal-line-landing .cmd { color: #e1e4ed; }
.terminal-line-landing .output-success { color: var(--success); }
.terminal-line-landing .output-info { color: var(--info); }
.terminal-line-landing .output-muted { color: var(--text-muted); }
.terminal-line-landing .blinking-cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--accent);
margin-left: 2px;
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes terminalFadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.scroll-indicator {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--text-muted);
font-size: 0.75rem;
animation: fadeSlideUp 1s ease-out 1.5s both;
}
.scroll-indicator .mouse {
width: 24px;
height: 38px;
border: 2px solid var(--text-muted);
border-radius: 12px;
position: relative;
opacity: 0.5;
}
.scroll-indicator .mouse::after {
content: '';
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 3px;
height: 10px;
background: var(--text-muted);
border-radius: 2px;
animation: scrollWheel 1.5s infinite;
}
@keyframes scrollWheel {
0% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
.features-section {
padding: 5rem 2rem;
max-width: 1100px;
margin: 0 auto;
}
.features-header {
text-align: center;
margin-bottom: 3.5rem;
}
.features-header h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.features-header p {
color: var(--text-muted);
font-size: 1.05rem;
max-width: 500px;
margin: 0 auto;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.25rem;
}
.feature-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.75rem;
transition: all 0.4s ease-out;
}
.feature-card:hover {
border-color: var(--accent);
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
}
.feature-card.visible:hover {
transform: translateY(-3px);
}
.feature-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
margin-bottom: 1.25rem;
}
.feature-icon.blue { background: rgba(59,130,246,0.15); color: var(--info); }
.feature-icon.green { background: rgba(34,197,94,0.15); color: var(--success); }
.feature-icon.purple { background: rgba(139,92,246,0.15); color: var(--purple); }
.feature-icon.orange { background: rgba(245,158,11,0.15); color: var(--warning); }
.feature-icon.red { background: rgba(239,68,68,0.15); color: var(--danger); }
.feature-icon.teal { background: rgba(20,184,166,0.15); color: #14b8a6; }
.feature-card h3 {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.feature-card p {
color: var(--text-muted);
font-size: 0.9rem;
line-height: 1.6;
}
.stats-section {
padding: 4rem 2rem 5rem;
text-align: center;
}
.stats-grid {
display: flex;
justify-content: center;
gap: 3rem;
flex-wrap: wrap;
max-width: 800px;
margin: 0 auto;
}
.stat-item {
opacity: 1;
transform: translateY(0);
transition: all 0.5s ease-out;
}
.stat-number {
font-size: 2.8rem;
font-weight: 800;
background: linear-gradient(135deg, var(--accent), var(--purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
color: var(--text-muted);
font-size: 0.9rem;
margin-top: 0.25rem;
}
.landing-footer {
text-align: center;
padding: 2rem;
border-top: 1px solid var(--border-color);
color: var(--text-muted);
font-size: 0.82rem;
}
.landing-footer a {
color: var(--text-muted);
text-decoration: none;
}
.landing-footer a:hover {
color: var(--accent);
}
.footer-links {
display: flex;
justify-content: center;
gap: 1.5rem;
margin-bottom: 0.75rem;
}
@media (max-width: 640px) {
.hero-title { font-size: 1.8rem; }
.features-grid { grid-template-columns: 1fr; }
.stats-grid { gap: 2rem; }
.stats-grid .stat-item { min-width: 120px; }
}
</style>
<div class="landing-page">
<section class="landing-hero">
<div class="floating-elements">
<div class="floating-elem"><i class="fas fa-server"></i></div>
<div class="floating-elem"><i class="fas fa-terminal"></i></div>
<div class="floating-elem"><i class="fas fa-database"></i></div>
<div class="floating-elem"><i class="fas fa-shield-alt"></i></div>
<div class="floating-elem"><i class="fas fa-hdd"></i></div>
<div class="floating-elem"><i class="fas fa-network-wired"></i></div>
<div class="floating-elem"><i class="fas fa-cloud"></i></div>
<div class="floating-elem"><i class="fas fa-microchip"></i></div>
</div>
<div class="hero-icon-wrapper">
<div class="hero-server-icon">
<i class="fas fa-server"></i>
</div>
</div>
<h1 class="hero-title">
Manage Your Servers<br>
<span class="gradient-text">From One Place</span>
</h1>
<p class="hero-subtitle">
ServerManager is a powerful, web-based platform for managing Linux servers.
Monitor, control, and deploy across your entire infrastructure from a single dashboard.
</p>
<div class="hero-cta">
<a href="/login" class="btn btn-primary-custom">
<i class="fas fa-sign-in-alt"></i> Sign In
</a>
<a href="/register" class="btn btn-secondary-custom">
<i class="fas fa-user-plus"></i> Create Account
</a>
</div>
<div class="hero-terminal">
<div class="terminal-bar">
<div class="terminal-dot red"></div>
<div class="terminal-dot yellow"></div>
<div class="terminal-dot green"></div>
<span class="terminal-title">servermanager ssh-session</span>
</div>
<div class="terminal-body-landing">
<div class="terminal-line-landing" style="animation-delay: 0.9s">
<span class="prompt">admin@servermanager:~$</span>
<span class="cmd"> systemctl status --all</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.1s">
<span class="output-success"> nginx active (running)</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.2s">
<span class="output-success"> mysql active (running)</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.3s">
<span class="output-success"> sshd active (running)</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.4s">
<span class="output-info"> redis inactive (dead)</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.5s">
<span class="prompt">admin@servermanager:~$</span>
<span class="cmd"> systemctl start redis</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.7s">
<span class="output-success"> redis active (running)</span>
</div>
<div class="terminal-line-landing" style="animation-delay: 1.8s">
<span class="output-muted">All services operational.</span>
<span class="blinking-cursor"></span>
</div>
</div>
</div>
<div class="scroll-indicator">
<div class="mouse"></div>
<span>Scroll to explore</span>
</div>
</section>
<section class="features-section" id="features">
<div class="features-header">
<h2>Everything You Need</h2>
<p>Complete server management toolkit at your fingertips</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon blue"><i class="fas fa-terminal"></i></div>
<h3>SSH Terminal</h3>
<p>Browser-based SSH terminal with real-time output, command history, and multi-session support.</p>
</div>
<div class="feature-card">
<div class="feature-icon green"><i class="fas fa-chart-line"></i></div>
<h3>Real-Time Monitoring</h3>
<p>Monitor CPU, memory, disk usage, and network traffic with live metrics and historical charts.</p>
</div>
<div class="feature-card">
<div class="feature-icon purple"><i class="fas fa-shield-alt"></i></div>
<h3>SSL Management</h3>
<p>Automate SSL certificate issuance and renewal via Let's Encrypt with full control.</p>
</div>
<div class="feature-card">
<div class="feature-icon orange"><i class="fas fa-globe"></i></div>
<h3>Nginx Manager</h3>
<p>Create, edit, and manage Nginx server blocks with syntax checking and site toggling.</p>
</div>
<div class="feature-card">
<div class="feature-icon red"><i class="fas fa-cogs"></i></div>
<h3>Service Control</h3>
<p>Start, stop, restart, and monitor system services across all your servers.</p>
</div>
<div class="feature-card">
<div class="feature-icon teal"><i class="fas fa-users-cog"></i></div>
<h3>Team Collaboration</h3>
<p>Assign role-based access to team members and delegate server management securely.</p>
</div>
</div>
</section>
<section class="stats-section" id="stats">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number" data-target="<?= (int) ($stats['servers'] ?? 0) ?>"><?= (int) ($stats['servers'] ?? 0) ?></div>
<div class="stat-label">Servers Managed</div>
</div>
<div class="stat-item">
<div class="stat-number" data-target="<?= (int) ($stats['users'] ?? 0) ?>"><?= (int) ($stats['users'] ?? 0) ?></div>
<div class="stat-label">Active Users</div>
</div>
<div class="stat-item">
<div class="stat-number" data-target="<?= (int) ($stats['online_servers'] ?? 0) ?>"><?= (int) ($stats['online_servers'] ?? 0) ?></div>
<div class="stat-label">Servers Online</div>
</div>
</div>
</section>
<footer class="landing-footer">
<div class="footer-links">
<a href="/privacy">Privacy Policy</a>
<a href="/terms">Terms & Conditions</a>
</div>
<p>&copy; <?= date('Y') ?> ServerManager. All rights reserved.</p>
</footer>
</div>
<script>
(function() {
document.querySelectorAll('.stat-number[data-target]').forEach(function(el) {
var target = parseInt(el.getAttribute('data-target'), 10);
el.textContent = target;
});
})();
</script>

31
views/pages/privacy.php Normal file
View File

@@ -0,0 +1,31 @@
<div class="legal-page">
<div class="legal-container">
<div style="max-width:960px;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)">
<h1 style="color:var(--text-primary);margin-bottom:0.25rem;font-size:1.8rem;font-weight:700">Privacy Policy</h1>
<p style="color:var(--text-muted);font-size:0.9rem;margin-bottom:2rem">Last updated: June 2026</p>
<hr style="border-color:var(--border-color);margin-bottom:2rem;opacity:0.5">
<div class="legal-content" style="line-height:1.8;color:var(--text-primary);font-size:1rem">
<?php
$lines = explode("\n", $content ?? '');
foreach ($lines as $line) {
$line = htmlspecialchars($line);
if (preg_match('/^[A-Z][A-Z\s]+$/', trim($line))) {
echo '<h2 style="color:var(--accent-primary);font-size:1.2rem;margin:2rem 0 1rem;font-weight:600">' . $line . '</h2>';
} elseif (preg_match('/^[a-z]\)\s/', trim($line))) {
echo '<p style="margin:0.4rem 0 0.4rem 2rem">' . $line . '</p>';
} elseif (preg_match('/^\d+\./', trim($line))) {
echo '<h3 style="color:var(--text-primary);font-size:1rem;margin:1.2rem 0 0.5rem;font-weight:600">' . $line . '</h3>';
} elseif (trim($line) === '') {
echo '<div style="height:0.5rem"></div>';
} else {
echo '<p style="margin:0.4rem 0">' . $line . '</p>';
}
}
?>
</div>
<div class="legal-footer" style="margin-top:2.5rem;padding-top:1.5rem;border-top:1px solid var(--border-color);text-align:center">
<p><a href="/" style="color:var(--accent-primary);text-decoration:none;font-size:0.95rem">&larr; Back to ServerManager</a></p>
</div>
</div>
</div>
</div>

33
views/pages/terms.php Normal file
View File

@@ -0,0 +1,33 @@
<div class="legal-page">
<div class="legal-container">
<div style="max-width:960px;margin:3rem auto;padding:3rem 3.5rem;background:var(--card-bg);border-radius:12px;box-shadow:0 2px 16px rgba(0,0,0,0.18)">
<h1 style="color:var(--text-primary);margin-bottom:0.25rem;font-size:1.8rem;font-weight:700">Terms & Conditions</h1>
<p style="color:var(--text-muted);font-size:0.9rem;margin-bottom:2rem">Last updated: June 2026</p>
<hr style="border-color:var(--border-color);margin-bottom:2rem;opacity:0.5">
<div class="legal-content" style="line-height:1.8;color:var(--text-primary);font-size:1rem">
<?php
$lines = explode("\n", $content ?? '');
foreach ($lines as $line) {
$line = htmlspecialchars($line);
if (preg_match('/^[A-Z][A-Z\s]+$/', trim($line))) {
echo '<h2 style="color:var(--accent-primary);font-size:1.2rem;margin:2rem 0 1rem;font-weight:600">' . $line . '</h2>';
} elseif (preg_match('/^\d+\.\d/', trim($line))) {
echo '<h3 style="color:var(--text-primary);font-size:1rem;margin:1rem 0 0.4rem 1.5rem;font-weight:600">' . $line . '</h3>';
} elseif (preg_match('/^\d+\./', trim($line))) {
echo '<h3 style="color:var(--text-primary);font-size:1rem;margin:1.2rem 0 0.5rem;font-weight:600">' . $line . '</h3>';
} elseif (preg_match('/^[-–—]\s/', trim($line))) {
echo '<p style="margin:0.4rem 0 0.4rem 2rem">' . $line . '</p>';
} elseif (trim($line) === '') {
echo '<div style="height:0.5rem"></div>';
} else {
echo '<p style="margin:0.4rem 0">' . $line . '</p>';
}
}
?>
</div>
<div class="legal-footer" style="margin-top:2.5rem;padding-top:1.5rem;border-top:1px solid var(--border-color);text-align:center">
<p><a href="/" style="color:var(--accent-primary);text-decoration:none;font-size:0.95rem">&larr; Back to ServerManager</a></p>
</div>
</div>
</div>
</div>