Init
This commit is contained in:
commit
60ca58f5ba
80 changed files with 9458 additions and 0 deletions
44
views/_footer.php
Normal file
44
views/_footer.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
</main>
|
||||
|
||||
<?php $b = buildInfo(); ?>
|
||||
<footer class="text-center text-muted small py-3 border-top mt-4">
|
||||
HackmanCMS
|
||||
<strong>v<?= htmlspecialchars($b['version']) ?></strong>
|
||||
<?php if ($b['sha'] !== ''): ?>
|
||||
· <code><?= htmlspecialchars($b['sha']) ?></code>
|
||||
<?php endif; ?>
|
||||
<?php if ($b['built'] !== ''): ?>
|
||||
· build <?= htmlspecialchars($b['built']) ?>
|
||||
<?php endif; ?>
|
||||
</footer>
|
||||
|
||||
<!-- Keyboard shortcut help (triggered with `?`) -->
|
||||
<div class="modal fade" id="shortcutsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title"><i class="bi bi-keyboard me-1"></i>Keyboard shortcuts</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body small">
|
||||
<table class="table table-sm mb-0">
|
||||
<tbody>
|
||||
<tr><td><kbd>?</kbd></td><td>Show this help</td></tr>
|
||||
<tr><td><kbd>g</kbd> <kbd>d</kbd></td><td>Go to dashboard</td></tr>
|
||||
<tr><td><kbd>g</kbd> <kbd>s</kbd></td><td>Go to settings</td></tr>
|
||||
<tr><td><kbd>g</kbd> <kbd>a</kbd></td><td>Go to audit log</td></tr>
|
||||
<tr><td><kbd>n</kbd></td><td>New post / draft (project page)</td></tr>
|
||||
<tr><td><kbd>b</kbd></td><td>Trigger generate (Hexo project page)</td></tr>
|
||||
<tr><td><kbd>Esc</kbd></td><td>Close any open modal</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($extra_scripts)) echo $extra_scripts; ?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
65
views/_header.php
Normal file
65
views/_header.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
$_nav_active = $nav_active ?? '';
|
||||
$_page_title = isset($page_title) ? htmlspecialchars($page_title) . ' — ' : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= $_page_title ?>HackmanCMS</title>
|
||||
<link rel="icon" type="image/png" href="/assets/img/logo.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg border-bottom px-3">
|
||||
<a class="navbar-brand fw-bold" href="/">
|
||||
<img src="/assets/img/logo.png" alt="" width="24" height="24" class="me-2">HackmanCMS
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $_nav_active === 'dashboard' ? 'active' : '' ?>" href="/">
|
||||
<i class="bi bi-grid-3x3-gap me-1"></i>Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $_nav_active === 'audit' ? 'active' : '' ?>" href="/audit">
|
||||
<i class="bi bi-journal-text me-1"></i>Audit
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php $user = Auth::currentUser(); ?>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle me-1"></i><?= htmlspecialchars($user['username'] ?? '') ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/settings">
|
||||
<i class="bi bi-gear me-2"></i>Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/audit">
|
||||
<i class="bi bi-journal-text me-2"></i>Audit
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="/api/auth?action=logout">
|
||||
<i class="bi bi-box-arrow-right me-2"></i>Log out
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container-fluid py-4">
|
||||
31
views/audit.php
Normal file
31
views/audit.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
$page_title = 'Audit Log';
|
||||
$nav_active = 'audit';
|
||||
include ROOT . '/views/_header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h4 mb-0">Audit Log</h2>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<select id="auditProjectFilter" class="form-select form-select-sm" style="width:auto">
|
||||
<option value="">All projects</option>
|
||||
<?php foreach ($db->query('SELECT id, name FROM projects WHERE is_active = 1 ORDER BY name') as $p): ?>
|
||||
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auditTable">
|
||||
<div class="text-muted small">Loading…</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-sm btn-outline-secondary d-none" id="auditPrev">
|
||||
<i class="bi bi-chevron-left me-1"></i>Prev
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary d-none" id="auditNext">
|
||||
Next<i class="bi bi-chevron-right ms-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php include ROOT . '/views/_footer.php'; ?>
|
||||
128
views/dashboard.php
Normal file
128
views/dashboard.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
$page_title = 'Dashboard';
|
||||
$nav_active = 'dashboard';
|
||||
include ROOT . '/views/_header.php';
|
||||
|
||||
$projects = $db->query('SELECT * FROM projects WHERE is_active = 1 ORDER BY is_pinned DESC, name')->fetchAll();
|
||||
?>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h4 mb-0">Projects</h2>
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addProjectModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (empty($projects)): ?>
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-folder-x fs-1 d-block mb-3 opacity-50"></i>
|
||||
<p>No projects yet.</p>
|
||||
<a href="/settings" class="btn btn-outline-secondary btn-sm">Configure scan paths</a>
|
||||
<button class="btn btn-primary btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#addProjectModal">Add manually</button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($projects as $proj):
|
||||
$type = ProjectTypes::get($proj['type']);
|
||||
$icon = $type ? $type::typeIcon() : 'bi-folder';
|
||||
$typeName = $type ? $type::typeName() : $proj['type'];
|
||||
$hasRun = $type && in_array('run', $type::tabs());
|
||||
?>
|
||||
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-2 gap-2">
|
||||
<i class="bi <?= htmlspecialchars($icon) ?> text-primary fs-5 flex-shrink-0"></i>
|
||||
<h6 class="card-title mb-0 text-truncate"><?= htmlspecialchars($proj['name']) ?></h6>
|
||||
</div>
|
||||
<p class="text-muted small mb-2 text-truncate" title="<?= htmlspecialchars($proj['path']) ?>">
|
||||
<code><?= htmlspecialchars($proj['path']) ?></code>
|
||||
</p>
|
||||
<div class="d-flex align-items-center gap-1 flex-wrap mt-1">
|
||||
<span class="badge bg-secondary"><?= htmlspecialchars($typeName) ?></span>
|
||||
<?php if ($proj['is_pinned']): ?>
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-pin-fill"></i></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($proj['url']): ?>
|
||||
<a href="<?= htmlspecialchars($proj['url']) ?>" target="_blank" rel="noopener"
|
||||
class="badge bg-dark text-decoration-none" title="Open site">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="badge bg-secondary-subtle text-body-secondary border project-disk d-none"
|
||||
data-project-id="<?= $proj['id'] ?>" title="Project size (excl. node_modules, public, .git)">
|
||||
<i class="bi bi-hdd me-1"></i><span class="project-disk-value">…</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex gap-2">
|
||||
<a href="/project/<?= $proj['id'] ?>" class="btn btn-sm btn-outline-primary flex-grow-1">Open</a>
|
||||
<?php if ($hasRun): ?>
|
||||
<a href="/project/<?= $proj['id'] ?>?tab=run" class="btn btn-sm btn-outline-secondary" title="Run commands">
|
||||
<i class="bi bi-terminal"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-sm btn-outline-secondary btn-pin-project flex-shrink-0"
|
||||
data-id="<?= $proj['id'] ?>"
|
||||
title="<?= $proj['is_pinned'] ? 'Unpin' : 'Pin' ?>">
|
||||
<i class="bi <?= $proj['is_pinned'] ? 'bi-pin-fill text-warning' : 'bi-pin' ?>"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Activity feed -->
|
||||
<div class="mt-5">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h6 class="mb-0"><i class="bi bi-journal-text me-1"></i>Recent activity</h6>
|
||||
<a href="/audit" class="ms-auto small text-decoration-none">View all →</a>
|
||||
</div>
|
||||
<div id="activityFeed" class="list-group list-group-flush">
|
||||
<div class="list-group-item text-muted small bg-transparent">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Project Modal -->
|
||||
<div class="modal fade" id="addProjectModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add project</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="addProjectForm">
|
||||
<div class="modal-body">
|
||||
<div id="addProjectError" class="alert alert-danger d-none"></div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Path on server</label>
|
||||
<input type="text" name="path" class="form-control" placeholder="/var/www/mysite" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<select name="type" class="form-select">
|
||||
<?php foreach (ProjectTypes::all() as $slug => $class): ?>
|
||||
<option value="<?= htmlspecialchars($slug) ?>"><?= htmlspecialchars($class::typeName()) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL <span class="text-muted">(optional)</span></label>
|
||||
<input type="url" name="url" class="form-control" placeholder="https://...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include ROOT . '/views/_footer.php'; ?>
|
||||
24
views/error.php
Normal file
24
views/error.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
$status = http_response_code() ?: 404;
|
||||
$messages = [404 => 'Page not found', 403 => 'Forbidden', 500 => 'Server error'];
|
||||
$msg = $messages[$status] ?? 'Something went wrong';
|
||||
if (Auth::check()):
|
||||
$page_title = $status;
|
||||
$nav_active = '';
|
||||
include ROOT . '/views/_header.php';
|
||||
?>
|
||||
<div class="text-center py-5">
|
||||
<p class="display-3 fw-bold text-muted"><?= $status ?></p>
|
||||
<p class="lead mb-4"><?= htmlspecialchars($msg) ?></p>
|
||||
<a href="/" class="btn btn-primary">Go home</a>
|
||||
</div>
|
||||
<?php
|
||||
include ROOT . '/views/_footer.php';
|
||||
else:
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head><meta charset="UTF-8"><title><?= $status ?></title></head>
|
||||
<body class="text-center pt-5"><?= $status ?> <?= htmlspecialchars($msg) ?></body>
|
||||
</html>
|
||||
<?php endif; ?>
|
||||
51
views/login.php
Normal file
51
views/login.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
$needs_setup = !Auth::hasUsers($db);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login — HackmanCMS</title>
|
||||
<link rel="icon" type="image/png" href="/assets/img/logo.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
</head>
|
||||
<body class="d-flex align-items-center justify-content-center min-vh-100">
|
||||
<div class="card" style="width:360px">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="card-title mb-4 text-center">
|
||||
<img src="/assets/img/logo.png" alt="" width="40" height="40" class="me-2">HackmanCMS
|
||||
</h4>
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="alert alert-danger py-2"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($needs_setup): ?>
|
||||
<p class="text-muted small">First run — create your admin account.</p>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="/api/auth">
|
||||
<input type="hidden" name="action" value="<?= $needs_setup ? 'setup' : 'login' ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" name="username" class="form-control" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
<?php if ($needs_setup): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Confirm password</label>
|
||||
<input type="password" name="password_confirm" class="form-control" required>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<?= $needs_setup ? 'Create account' : 'Log in' ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
105
views/project/_tab_analytics.php
Normal file
105
views/project/_tab_analytics.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
$pStmt = $db->prepare('SELECT key, value FROM project_settings WHERE project_id = ?');
|
||||
$pStmt->execute([$pid]);
|
||||
$pSettings = [];
|
||||
foreach ($pStmt->fetchAll() as $row) $pSettings[$row['key']] = $row['value'];
|
||||
?>
|
||||
<div id="analyticsPanel" data-project-id="<?= $pid ?>">
|
||||
|
||||
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
||||
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Analytics</h5>
|
||||
<select id="analyticsRange" class="form-select form-select-sm py-0 ms-auto" style="width:auto">
|
||||
<option value="7" selected>Last 7 days</option>
|
||||
<option value="30">Last 30 days</option>
|
||||
<option value="90">Last 90 days</option>
|
||||
<option value="365">Last year</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="analyticsRefreshBtn" title="Refresh">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="analyticsBody" class="text-muted small">Loading…</div>
|
||||
|
||||
<!-- Setup / log import — collapsed by default. -->
|
||||
<details class="mt-4" id="analyticsSetupDetails">
|
||||
<summary class="small text-muted">
|
||||
<i class="bi bi-gear me-1"></i>Server-log import setup
|
||||
</summary>
|
||||
<div class="mt-2">
|
||||
<p class="text-muted small mb-2">
|
||||
Imports page-view data from the web server's access log. No JS, no pixel,
|
||||
no client-side change to the managed site. Set the path of the access log
|
||||
below; HackmanCMS tails new lines on each import. Asset hits, non-GETs and
|
||||
obvious bots are dropped.
|
||||
</p>
|
||||
<div id="analyticsSettings" data-project-id="<?= $pid ?>" class="mb-2">
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small">Access log file</label>
|
||||
<input type="text" id="analyticsLogPath" class="form-control form-control-sm font-monospace"
|
||||
value="<?= htmlspecialchars($pSettings['analytics_log_path'] ?? '') ?>"
|
||||
placeholder="/var/log/apache2/blog.example.com_access.log">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Log format</label>
|
||||
<select id="analyticsLogFormat" class="form-select form-select-sm">
|
||||
<?php $fmt = $pSettings['analytics_log_format'] ?? 'combined'; ?>
|
||||
<option value="combined" <?= $fmt === 'combined' ? 'selected' : '' ?>>Apache combined</option>
|
||||
<option value="nginx" <?= $fmt === 'nginx' ? 'selected' : '' ?>>nginx default</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-sm btn-outline-secondary w-100" id="analyticsSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small">Path prefix filter <span class="text-muted">(optional)</span></label>
|
||||
<input type="text" id="analyticsLogFilter" class="form-control form-control-sm font-monospace"
|
||||
value="<?= htmlspecialchars($pSettings['analytics_log_filter'] ?? '') ?>"
|
||||
placeholder="/blog">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small d-block"> </label>
|
||||
<button class="btn btn-sm btn-primary w-100" id="analyticsImportBtn">
|
||||
<i class="bi bi-arrow-down-circle me-1"></i>Import now
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-sm btn-outline-warning w-100" id="analyticsResetBtn"
|
||||
title="Discard last-position state and reimport from start">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="analyticsStatus" class="small text-muted">
|
||||
<?php if (isset($pSettings['analytics_imported_at'])): ?>
|
||||
Last import: <?= htmlspecialchars($pSettings['analytics_imported_at']) ?>
|
||||
(<?= htmlspecialchars($pSettings['analytics_imported_count'] ?? '0') ?> rows total).
|
||||
<?php else: ?>
|
||||
No imports yet.
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="text-muted small mb-0 mt-2">
|
||||
Continuous tracking — install once as a user that can read the log:
|
||||
<br>
|
||||
<code class="small">*/5 * * * * /usr/bin/php /opt/hackmancms/bin/import-site-logs.php</code>
|
||||
</p>
|
||||
<p class="text-muted small mb-0 mt-2">
|
||||
<i class="bi bi-archive"></i>
|
||||
<strong>Retention:</strong> raw events for 90 days, hourly rollups
|
||||
for 365 days, daily rollups forever. Visitor IPs are hashed with a
|
||||
<strong>daily-rotating salt</strong> so visitors look like new
|
||||
visitors across days — "uniques over a multi-day window" is
|
||||
therefore the sum of per-day unique counts.
|
||||
<?php if (isset($pSettings['analytics_last_rollup'])): ?>
|
||||
Last rollup:
|
||||
<?= htmlspecialchars($pSettings['analytics_last_rollup']) ?>.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
15
views/project/_tab_config.php
Normal file
15
views/project/_tab_config.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div id="configPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 gap-2 flex-wrap">
|
||||
<select id="configFileSelect" class="form-select form-select-sm" style="width:auto;min-width:220px">
|
||||
<option value="_config.yml">_config.yml (blog)</option>
|
||||
</select>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span id="configSaveStatus" class="text-muted small"></span>
|
||||
<button class="btn btn-sm btn-primary" id="configSaveBtn">
|
||||
<i class="bi bi-floppy me-1"></i>Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="configEditor"
|
||||
style="border:1px solid var(--hm-border);border-radius:4px;overflow:hidden"></div>
|
||||
</div>
|
||||
44
views/project/_tab_dashboard.php
Normal file
44
views/project/_tab_dashboard.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<div id="projectDashboard" data-project-id="<?= $pid ?>"
|
||||
data-project-type="<?= htmlspecialchars($project['type']) ?>">
|
||||
|
||||
<?php if ($isHexo): ?>
|
||||
<!-- Tags + categories -->
|
||||
<div id="tagsPanel" data-project-id="<?= $pid ?>" class="mb-4">
|
||||
<div id="tagsLoading" class="text-muted small">Loading tags…</div>
|
||||
<div id="tagsContent" class="d-none">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-2 text-muted">
|
||||
<i class="bi bi-tags me-1"></i>Tags
|
||||
<span id="tagsCount" class="badge bg-secondary ms-1">0</span>
|
||||
</h6>
|
||||
<div id="tagCloud" class="tag-cloud"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-2 text-muted">
|
||||
<i class="bi bi-folder me-1"></i>Categories
|
||||
<span id="catsCount" class="badge bg-secondary ms-1">0</span>
|
||||
</h6>
|
||||
<div id="catCloud" class="tag-cloud"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Recent files -->
|
||||
<div id="recentPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex align-items-center mb-2 gap-2">
|
||||
<h6 class="mb-0 text-muted">
|
||||
<i class="bi bi-clock-history me-1"></i>Recent files
|
||||
</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-auto py-0" id="recentClearBtn">
|
||||
<i class="bi bi-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<div id="recentList" class="list-group">
|
||||
<div class="list-group-item text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
21
views/project/_tab_drafts.php
Normal file
21
views/project/_tab_drafts.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<div id="draftsPanel" class="row g-0 h-split" data-project-id="<?= $pid ?>">
|
||||
<!-- Left: draft list -->
|
||||
<div class="col-md-5 col-xl-4 split-list pe-2">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-sm btn-primary" id="newDraftBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>New draft
|
||||
</button>
|
||||
</div>
|
||||
<div id="draftsList">
|
||||
<div class="text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: draft editor -->
|
||||
<div class="col-md-7 col-xl-8 border-start ps-3" id="draftEditorPane">
|
||||
<div class="editor-placeholder">
|
||||
<i class="bi bi-pencil-square fs-1 d-block mb-2 opacity-25"></i>
|
||||
<span>Select a draft or create a new one</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
84
views/project/_tab_git.php
Normal file
84
views/project/_tab_git.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<div id="gitPanel" data-project-id="<?= $pid ?>">
|
||||
<!-- Subdir picker (hidden until needed) -->
|
||||
<div id="gitSubdirSelector" class="d-none alert alert-info py-2 d-flex align-items-center gap-3 mb-3">
|
||||
<i class="bi bi-git flex-shrink-0"></i>
|
||||
<span class="flex-grow-1 small">No git repo at project root. Found in subdirectory:</span>
|
||||
<select id="gitSubdirSelect" class="form-select form-select-sm flex-shrink-0" style="width:auto"></select>
|
||||
<button class="btn btn-sm btn-primary flex-shrink-0" id="gitSubdirUseBtn">Use</button>
|
||||
</div>
|
||||
|
||||
<div id="gitNoRepo" class="alert alert-warning d-none">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>Not a git repository and no git repos found in subdirectories.
|
||||
</div>
|
||||
|
||||
<div id="gitStatus" class="d-none">
|
||||
<!-- Branch + summary -->
|
||||
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap">
|
||||
<span class="badge bg-secondary fs-6" id="gitBranch"><i class="bi bi-git me-1"></i>—</span>
|
||||
<span class="text-muted small" id="gitStatusSummary"></span>
|
||||
<div class="ms-auto d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="gitStashBtn">Stash</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="gitStashPopBtn">Stash pop</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="gitResetBtn">Reset HEAD</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Changed files -->
|
||||
<div id="gitFiles" class="mb-2"></div>
|
||||
|
||||
<!-- Commit form -->
|
||||
<div class="input-group mb-3" style="max-width:700px">
|
||||
<input type="text" id="gitCommitMsg" class="form-control form-control-sm"
|
||||
placeholder="Commit message — stages all changed files">
|
||||
<button class="btn btn-sm btn-primary" id="gitCommitBtn">
|
||||
<i class="bi bi-check2 me-1"></i>Commit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pull / Push -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="gitPullBtn">
|
||||
<i class="bi bi-cloud-download me-1"></i>Pull
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="gitPushBtn">
|
||||
<i class="bi bi-cloud-upload me-1"></i>Push
|
||||
</button>
|
||||
</div>
|
||||
<pre id="gitStreamOutput" class="small p-2 rounded d-none"
|
||||
style="max-height:200px;overflow-y:auto;background:var(--hm-bg)"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Sub-tabs -->
|
||||
<div class="d-flex gap-2 mb-3 mt-2">
|
||||
<button class="btn btn-sm btn-outline-primary active" id="gitTabLogBtn">Log</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="gitTabBranchesBtn">Branches</button>
|
||||
</div>
|
||||
|
||||
<div id="gitLogPanel">
|
||||
<div id="gitLogTable" class="text-muted small">Loading…</div>
|
||||
</div>
|
||||
<div id="gitBranchesPanel" class="d-none">
|
||||
<div id="gitBranchList" class="mb-3"></div>
|
||||
<div class="input-group input-group-sm" style="max-width:400px">
|
||||
<input type="text" id="gitNewBranch" class="form-control" placeholder="new-branch-name">
|
||||
<button class="btn btn-outline-secondary" id="gitCreateBranchBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Create & switch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diff viewer modal -->
|
||||
<div class="modal fade" id="gitDiffModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="gitDiffTitle">Diff</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<pre id="gitDiffContent" class="diff-view m-0 p-3 small" style="overflow-x:auto"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
30
views/project/_tab_links.php
Normal file
30
views/project/_tab_links.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<div id="linksPanel" data-project-id="<?= $pid ?>"
|
||||
data-project-url="<?= htmlspecialchars($project['url'] ?? '') ?>">
|
||||
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
||||
<i class="bi bi-link-45deg text-primary"></i>
|
||||
<span class="small text-muted">
|
||||
Scans posts/pages for HTTP/HTTPS links (and internal links if a project URL is set)
|
||||
and reports any that are unreachable.
|
||||
</span>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox" id="linksOnlyBroken" checked>
|
||||
<label class="form-check-label small" for="linksOnlyBroken">Broken only</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" id="linksScanBtn">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Run scan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (!($project['url'] ?? '')): ?>
|
||||
<div class="alert alert-warning small py-2">
|
||||
No project URL is set. Internal links (starting with <code>/</code>) and relative
|
||||
links will be skipped. Set a URL on Settings → Display name area or via project edit
|
||||
to enable them.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="linksRunInfo" class="text-muted small mb-2"></div>
|
||||
<div id="linksTableWrap" class="table-responsive">
|
||||
<div class="text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
11
views/project/_tab_notes.php
Normal file
11
views/project/_tab_notes.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div id="scratchpadCard" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex align-items-center mb-2 gap-2">
|
||||
<i class="bi bi-sticky text-warning"></i>
|
||||
<span class="small text-muted">Quick notes — pre-deploy TODOs, reminders. Auto-saves.</span>
|
||||
<span id="scratchpadStatus" class="text-muted small ms-auto"></span>
|
||||
</div>
|
||||
<textarea id="scratchpadInput"
|
||||
class="form-control font-monospace"
|
||||
style="height: calc(var(--hm-tab-height) - 30px)"
|
||||
placeholder="Start typing — saves automatically…"><?= htmlspecialchars((string)($project['scratchpad'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
27
views/project/_tab_plugins.php
Normal file
27
views/project/_tab_plugins.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<div id="pluginsPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
||||
<i class="bi bi-puzzle text-primary"></i>
|
||||
<span class="small text-muted">
|
||||
Hexo plugins listed from <code>package.json</code>. Install/uninstall runs npm.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input type="text" id="pluginInstallName" class="form-control form-control-sm font-monospace"
|
||||
placeholder="hexo-…" style="max-width:300px">
|
||||
<button class="btn btn-sm btn-primary" id="pluginInstallBtn">
|
||||
<i class="bi bi-cloud-download me-1"></i>Install
|
||||
</button>
|
||||
<small class="text-muted ms-2">npm install --save</small>
|
||||
</div>
|
||||
<div id="pluginInstallLog" class="small font-monospace text-muted mt-2 d-none"
|
||||
style="white-space:pre-wrap;max-height:200px;overflow:auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pluginsList" class="row g-2">
|
||||
<div class="col-12 text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
12
views/project/_tab_recent.php
Normal file
12
views/project/_tab_recent.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<div id="recentPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex align-items-center mb-3 gap-2">
|
||||
<i class="bi bi-clock-history text-info"></i>
|
||||
<span class="small text-muted">Recently opened or saved files in this project.</span>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-auto" id="recentClearBtn">
|
||||
<i class="bi bi-trash"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<div id="recentList" class="list-group">
|
||||
<div class="list-group-item text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
9
views/project/_tab_search.php
Normal file
9
views/project/_tab_search.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div id="searchPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="input-group mb-3" style="max-width:600px">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" id="searchQuery" class="form-control"
|
||||
placeholder="Search in markdown files…" autocomplete="off">
|
||||
<button class="btn btn-outline-secondary" id="searchClearBtn">Clear</button>
|
||||
</div>
|
||||
<div id="searchResults"></div>
|
||||
</div>
|
||||
231
views/project/_tab_settings.php
Normal file
231
views/project/_tab_settings.php
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
// Load current project settings from DB
|
||||
$psStmt = $db->prepare('SELECT key, value FROM project_settings WHERE project_id = ?');
|
||||
$psStmt->execute([$pid]);
|
||||
$pSettings = [];
|
||||
foreach ($psStmt->fetchAll() as $row) {
|
||||
$pSettings[$row['key']] = $row['value'];
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$schedules = [];
|
||||
if ($isHexo) {
|
||||
$sStmt = $db->prepare('SELECT * FROM scheduled_builds WHERE project_id = ? ORDER BY id');
|
||||
$sStmt->execute([$pid]);
|
||||
$schedules = $sStmt->fetchAll();
|
||||
}
|
||||
?>
|
||||
<div id="settingsPanel" data-project-id="<?= $pid ?>">
|
||||
|
||||
<!-- Project -->
|
||||
<h6 class="mb-3">Project</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Display name</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="projectNameInput" class="form-control"
|
||||
value="<?= htmlspecialchars($project['name']) ?>">
|
||||
<button class="btn btn-outline-secondary" id="projectNameSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Project type</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<select class="form-select" id="projectTypeSelectSettings">
|
||||
<?php foreach (ProjectTypes::all() as $slug => $class): ?>
|
||||
<option value="<?= htmlspecialchars($slug) ?>" <?= $project['type'] === $slug ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($class::typeName()) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" id="projectTypeSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($isHexo): ?>
|
||||
<hr>
|
||||
|
||||
<!-- Broken link checker -->
|
||||
<h6 class="mb-2 mt-3">Broken link checker</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
Scans posts/pages for HTTP/HTTPS links (and internal links if a project URL is set)
|
||||
and reports any that are unreachable.
|
||||
</p>
|
||||
<div id="linksPanel" class="mb-4"
|
||||
data-project-id="<?= $pid ?>"
|
||||
data-project-url="<?= htmlspecialchars($project['url'] ?? '') ?>">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<button class="btn btn-sm btn-primary" id="linksScanBtn">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Run scan
|
||||
</button>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input class="form-check-input" type="checkbox" id="linksOnlyBroken" checked>
|
||||
<label class="form-check-label small" for="linksOnlyBroken">Broken only</label>
|
||||
</div>
|
||||
<span id="linksRunInfo" class="text-muted small ms-auto"></span>
|
||||
</div>
|
||||
<div id="linksTableWrap" class="table-responsive"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Backup & Export -->
|
||||
<h6 class="mb-2 mt-3">Backup & export</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
Download a zip of the project source.
|
||||
Excludes <code>node_modules</code>, <code>public</code>, and <code>.git</code>.
|
||||
</p>
|
||||
<a class="btn btn-sm btn-outline-primary mb-4"
|
||||
href="/api/backup?project_id=<?= $pid ?>">
|
||||
<i class="bi bi-download me-1"></i>Download backup (.zip)
|
||||
</a>
|
||||
|
||||
<?php if ($isHexo): ?>
|
||||
<hr>
|
||||
|
||||
<!-- Scheduled builds (Hexo) -->
|
||||
<h6 class="mb-2 mt-3">Scheduled builds</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
Run a project command on a cron schedule. Output is saved to command history.
|
||||
Requires the system cron entry to be installed (see <code>docs/scheduled-builds.md</code>).
|
||||
</p>
|
||||
<div id="schedulesList" class="mb-2">
|
||||
<?php if (!$schedules): ?>
|
||||
<div class="text-muted small">No schedules yet.</div>
|
||||
<?php else: foreach ($schedules as $s): ?>
|
||||
<div class="d-flex align-items-center gap-2 border rounded p-2 mb-1 schedule-row"
|
||||
data-id="<?= (int)$s['id'] ?>">
|
||||
<div class="form-check form-switch m-0">
|
||||
<input class="form-check-input schedule-enabled" type="checkbox"
|
||||
<?= $s['is_enabled'] ? 'checked' : '' ?>>
|
||||
</div>
|
||||
<select class="form-select form-select-sm schedule-cmd" style="max-width:150px">
|
||||
<?php foreach ($type::commands() as $c): ?>
|
||||
<option value="<?= htmlspecialchars($c['id']) ?>"
|
||||
<?= $s['cmd_id'] === $c['id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($c['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type="text" class="form-control form-control-sm font-monospace schedule-cron"
|
||||
placeholder="0 3 * * *" value="<?= htmlspecialchars($s['cron']) ?>"
|
||||
style="max-width:160px">
|
||||
<small class="text-muted small flex-grow-1">
|
||||
<?php if ($s['last_run_at']): ?>
|
||||
last: <?= htmlspecialchars($s['last_run_at']) ?>
|
||||
(<?= htmlspecialchars($s['last_status'] ?? '–') ?>)
|
||||
<?php else: ?>
|
||||
never run
|
||||
<?php endif; ?>
|
||||
</small>
|
||||
<button class="btn btn-sm btn-outline-secondary schedule-save" title="Save">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger schedule-delete" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="newScheduleBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add schedule
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Page directories (Hexo) -->
|
||||
<h6 class="mb-2 mt-3">Page directories</h6>
|
||||
<p class="text-muted small mb-2">
|
||||
Subdirectories of <code>source/</code> scanned for pages, one per line.
|
||||
Leave empty to scan only <code>source/*.md</code>.
|
||||
</p>
|
||||
<textarea id="pageDirsInput" class="form-control form-control-sm font-monospace mb-2" rows="3"
|
||||
placeholder="p pages"><?= htmlspecialchars($pSettings['page_dirs'] ?? "p\npages") ?></textarea>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="pageDirsSaveBtn">Save</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Templates -->
|
||||
<h6 class="mb-3 mt-3">Post Templates</h6>
|
||||
<p class="text-muted small">
|
||||
Full post file content (front matter + body). Applied when creating a new post.
|
||||
</p>
|
||||
<div id="templatesList" class="mb-3"></div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="newTemplateBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>New template
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Snippets -->
|
||||
<h6 class="mb-3 mt-3">Snippets</h6>
|
||||
<p class="text-muted small">
|
||||
Reusable markdown fragments. Insert into post body via the snippet picker.
|
||||
</p>
|
||||
<div id="snippetsList" class="mb-3"></div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="newSnippetBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>New snippet
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($isHexo): ?>
|
||||
<!-- Template editor modal -->
|
||||
<div class="modal fade" id="templateModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="templateModalTitle">New template</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="templateId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" id="templateName" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small">Content <span class="text-muted">(full post: front matter + body)</span></label>
|
||||
<div id="templateContentEditor" class="border rounded" style="height:400px"></div>
|
||||
<textarea id="templateContent" style="display:none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" id="templateSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snippet editor modal -->
|
||||
<div class="modal fade" id="snippetModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="snippetModalTitle">New snippet</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="snippetId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" id="snippetName" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small">Content</label>
|
||||
<div id="snippetContentEditor" class="border rounded" style="height:250px"></div>
|
||||
<textarea id="snippetContent" style="display:none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" id="snippetSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
19
views/project/_tab_tags.php
Normal file
19
views/project/_tab_tags.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div id="tagsPanel" data-project-id="<?= $pid ?>">
|
||||
<div id="tagsLoading" class="text-muted small">Loading…</div>
|
||||
<div id="tagsContent" class="d-none">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3 text-muted">
|
||||
Tags <span id="tagsCount" class="badge bg-secondary ms-1">0</span>
|
||||
</h6>
|
||||
<div id="tagCloud" class="tag-cloud"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3 text-muted">
|
||||
Categories <span id="catsCount" class="badge bg-secondary ms-1">0</span>
|
||||
</h6>
|
||||
<div id="catCloud" class="tag-cloud"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
46
views/project/_tab_themes.php
Normal file
46
views/project/_tab_themes.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<div id="themesPanel" data-project-id="<?= $pid ?>">
|
||||
<div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
|
||||
<i class="bi bi-palette text-primary"></i>
|
||||
<span class="small text-muted">
|
||||
Manage Hexo themes installed under <code>themes/</code>. Switching writes to <code>_config.yml</code>.
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-auto" id="cloneThemeBtn" data-bs-toggle="modal"
|
||||
data-bs-target="#cloneThemeModal">
|
||||
<i class="bi bi-cloud-download me-1"></i>Clone from git URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="themesList" class="row g-3">
|
||||
<div class="col-12 text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clone modal -->
|
||||
<div class="modal fade" id="cloneThemeModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title">Clone theme</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Git URL</label>
|
||||
<input type="text" id="cloneThemeUrl" class="form-control form-control-sm font-monospace"
|
||||
placeholder="https://github.com/user/hexo-theme-foo.git">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Folder name <span class="text-muted">(optional)</span></label>
|
||||
<input type="text" id="cloneThemeName" class="form-control form-control-sm"
|
||||
placeholder="auto from URL">
|
||||
</div>
|
||||
<div id="cloneThemeLog" class="small font-monospace text-muted"
|
||||
style="white-space:pre-wrap;max-height:200px;overflow:auto"></div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary" id="cloneThemeSubmit">Clone</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
431
views/project/view.php
Normal file
431
views/project/view.php
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
<?php
|
||||
$stmt = $db->prepare('SELECT * FROM projects WHERE id = ?');
|
||||
$stmt->execute([$project_id]);
|
||||
$project = $stmt->fetch();
|
||||
if (!$project) { http_response_code(404); include ROOT . '/views/error.php'; exit; }
|
||||
|
||||
$type = ProjectTypes::get($project['type']);
|
||||
$tabs = $type ? $type::tabs() : ['files'];
|
||||
$isHexo = $project['type'] === 'hexo';
|
||||
$isStorage = $project['type'] === 'storage';
|
||||
$pid = (int)$project['id'];
|
||||
|
||||
// Remove 'git' tab if no git repo found at project root or one level deep
|
||||
if (in_array('git', $tabs)) {
|
||||
$projectPath = $project['path'];
|
||||
$hasGit = is_dir($projectPath . '/.git');
|
||||
if (!$hasGit) {
|
||||
foreach (@scandir($projectPath) ?: [] as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
if (is_dir($projectPath . '/' . $item . '/.git')) { $hasGit = true; break; }
|
||||
}
|
||||
}
|
||||
if (!$hasGit) $tabs = array_values(array_diff($tabs, ['git']));
|
||||
}
|
||||
|
||||
$tab = $_GET['tab'] ?? $tabs[0];
|
||||
if (!in_array($tab, $tabs)) $tab = $tabs[0];
|
||||
|
||||
$page_title = $project['name'];
|
||||
$nav_active = '';
|
||||
include ROOT . '/views/_header.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
$tabLabels = [
|
||||
'dashboard' => 'Dashboard', 'analytics' => 'Analytics',
|
||||
'posts' => 'Posts', 'config' => 'Config',
|
||||
'files' => 'Files', 'media' => 'Media',
|
||||
'run' => 'Run', 'themes' => 'Themes',
|
||||
'plugins' => 'Plugins', 'git' => 'Git',
|
||||
'notes' => 'Notes', 'settings' => 'Settings',
|
||||
];
|
||||
$tabIcons = [
|
||||
'dashboard' => 'bi-grid-1x2', 'analytics' => 'bi-graph-up',
|
||||
'posts' => 'bi-file-earmark-text','config' => 'bi-sliders',
|
||||
'files' => 'bi-folder2', 'media' => 'bi-images',
|
||||
'run' => 'bi-terminal', 'themes' => 'bi-palette',
|
||||
'plugins' => 'bi-puzzle', 'git' => 'bi-git',
|
||||
'notes' => 'bi-sticky', 'settings' => 'bi-gear',
|
||||
];
|
||||
$tabGroups = [
|
||||
['dashboard', 'analytics'],
|
||||
['posts', 'config', 'files', 'media'],
|
||||
['run', 'themes', 'plugins', 'git'],
|
||||
['notes', 'settings'],
|
||||
];
|
||||
?>
|
||||
|
||||
<!-- Sidebar + content -->
|
||||
<div class="project-layout d-flex gap-3 align-items-start">
|
||||
<aside class="project-sidebar" id="projectSidebar">
|
||||
|
||||
<button type="button" class="btn btn-sm btn-link text-body-secondary p-0 sidebar-toggle"
|
||||
id="sidebarToggle" title="Collapse sidebar" aria-label="Collapse sidebar">
|
||||
<i class="bi bi-chevron-double-left collapse-icon-expanded"></i>
|
||||
<i class="bi bi-chevron-double-right collapse-icon-collapsed"></i>
|
||||
</button>
|
||||
|
||||
<!-- Project meta header -->
|
||||
<div class="project-sidebar-header mb-2">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<i class="bi <?= htmlspecialchars($type ? $type::typeIcon() : 'bi-folder') ?> text-primary flex-shrink-0"></i>
|
||||
<span class="fw-semibold text-truncate sidebar-label" title="<?= htmlspecialchars($project['path']) ?>">
|
||||
<?= htmlspecialchars($project['name']) ?>
|
||||
</span>
|
||||
<div class="dropdown ms-auto flex-shrink-0 sidebar-label">
|
||||
<button class="btn btn-sm btn-link text-body-secondary p-0 px-1"
|
||||
data-bs-toggle="dropdown" aria-expanded="false" title="More">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><span class="dropdown-item-text small text-muted text-truncate d-block" style="max-width:280px"
|
||||
title="<?= htmlspecialchars($project['path']) ?>">
|
||||
<code><?= htmlspecialchars($project['path']) ?></code>
|
||||
</span></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item text-danger"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteProjectModal">
|
||||
<i class="bi bi-trash me-2"></i>Remove project
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($project['url']): ?>
|
||||
<a href="<?= htmlspecialchars($project['url']) ?>" target="_blank" rel="noopener"
|
||||
class="d-inline-flex align-items-center gap-1 small text-decoration-none sidebar-label mt-1"
|
||||
title="<?= htmlspecialchars($project['url']) ?>">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
<span class="text-truncate"><?= htmlspecialchars(preg_replace('#^https?://#', '', $project['url'])) ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="mt-1 sidebar-label">
|
||||
<span class="badge bg-secondary-subtle text-body-secondary border d-none"
|
||||
id="diskBadge" data-project-id="<?= $pid ?>"
|
||||
title="Project size (excl. node_modules, public, .git)">
|
||||
<i class="bi bi-hdd me-1"></i><span id="diskBadgeValue">…</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Grouped nav -->
|
||||
<?php foreach ($tabGroups as $i => $group):
|
||||
$visible = array_values(array_intersect($group, $tabs));
|
||||
if (!$visible) continue; ?>
|
||||
<?php if ($i > 0): ?><hr><?php endif; ?>
|
||||
<ul class="nav flex-column">
|
||||
<?php foreach ($visible as $t): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $t === $tab ? 'active' : '' ?>"
|
||||
href="?tab=<?= urlencode($t) ?>"
|
||||
title="<?= htmlspecialchars($tabLabels[$t] ?? ucfirst($t)) ?>">
|
||||
<i class="bi <?= $tabIcons[$t] ?? 'bi-circle' ?>"></i>
|
||||
<span class="sidebar-label"><?= htmlspecialchars($tabLabels[$t] ?? ucfirst($t)) ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</aside>
|
||||
|
||||
<section class="project-main flex-grow-1 min-w-0">
|
||||
|
||||
<!-- ── POSTS tab (Hexo) ─────────────────────────────────────────────────────── -->
|
||||
<?php if ($tab === 'posts'): ?>
|
||||
<div id="postsPanel" class="row g-0 h-split" data-project-id="<?= $pid ?>">
|
||||
<div class="col-md-5 col-xl-4 split-list pe-2">
|
||||
<div class="d-flex align-items-center gap-1 mb-2 flex-wrap">
|
||||
<button class="btn btn-sm btn-outline-primary active" id="showPosts">Posts</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="showPages">Pages</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="showDrafts">Drafts</button>
|
||||
<button class="btn btn-sm btn-primary" id="newItemBtn" title="New draft">
|
||||
<i class="bi bi-plus-lg"></i> New
|
||||
</button>
|
||||
<div class="input-group input-group-sm ms-auto" style="max-width:160px">
|
||||
<input type="text" id="searchQuery" class="form-control" placeholder="Search…" autocomplete="off">
|
||||
<button class="btn btn-outline-secondary" id="searchClearBtn" title="Clear"><i class="bi bi-x-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="postsList"></div>
|
||||
<div id="searchResults" class="d-none"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7 col-xl-8 border-start ps-3 d-flex flex-column">
|
||||
<div class="editor-tab-bar" id="editorTabBar"></div>
|
||||
<div class="editor-tab-content" id="editorTabContent">
|
||||
<div class="editor-placeholder">
|
||||
<i class="bi bi-cursor-text fs-1 d-block mb-2 opacity-25"></i>
|
||||
Select a post to edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── FILES tab ─────────────────────────────────────────────────────────────── -->
|
||||
<?php elseif ($tab === 'files'): ?>
|
||||
<div id="fileBrowser" class="row g-0 h-split"
|
||||
data-project-id="<?= $pid ?>"
|
||||
data-project-type="<?= htmlspecialchars($project['type']) ?>">
|
||||
<div class="col-md-5 col-xl-4 split-list pe-2">
|
||||
<div class="d-flex align-items-center mb-2 gap-2">
|
||||
<nav aria-label="breadcrumb" id="fileCrumb" class="flex-grow-1">
|
||||
<ol class="breadcrumb mb-0 small">
|
||||
<li class="breadcrumb-item"><a href="#" data-path="">Root</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<button class="btn btn-xs btn-outline-secondary flex-shrink-0"
|
||||
data-bs-toggle="modal" data-bs-target="#uploadModal"
|
||||
title="Upload file">
|
||||
<i class="bi bi-cloud-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="fileList" class="list-group">
|
||||
<div class="list-group-item text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 col-xl-8 border-start ps-3 d-flex flex-column" id="editorPane">
|
||||
<div class="editor-tab-bar" id="editorTabBar"></div>
|
||||
<div class="editor-tab-content" id="editorTabContent">
|
||||
<div class="editor-placeholder">
|
||||
<i class="bi bi-cursor-text fs-1 d-block mb-2 opacity-25"></i>
|
||||
Select a file to edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── RUN tab ───────────────────────────────────────────────────────────────── -->
|
||||
<?php elseif ($tab === 'run' && $type && in_array('run', $tabs)): ?>
|
||||
<div id="commandRunner" data-project-id="<?= $pid ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-header small">Commands</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($type::commands() as $cmd): ?>
|
||||
<button class="list-group-item list-group-item-action btn-run-cmd"
|
||||
data-cmd="<?= htmlspecialchars($cmd['id']) ?>">
|
||||
<span class="d-block"><?= htmlspecialchars($cmd['label']) ?></span>
|
||||
<code class="small text-muted"><?= htmlspecialchars($cmd['cmd']) ?></code>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 col-lg-9">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center small">
|
||||
Output
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary py-0" id="showHistoryBtn">History</button>
|
||||
<button class="btn btn-sm btn-outline-secondary py-0" id="clearOutput">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<pre id="cmdOutput" class="m-0 p-3 text-success"
|
||||
style="min-height:300px;max-height:65vh;overflow-y:auto;font-size:.8rem;background:transparent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── MEDIA tab (Storage) ───────────────────────────────────────────────────── -->
|
||||
<?php elseif ($tab === 'media'): ?>
|
||||
<div id="mediaPanel" data-project-id="<?= $pid ?>"
|
||||
data-project-url="<?= htmlspecialchars($project['url'] ?? '') ?>">
|
||||
<nav aria-label="breadcrumb" id="mediaCrumb" class="mb-3">
|
||||
<ol class="breadcrumb mb-0 small">
|
||||
<li class="breadcrumb-item"><a href="#" data-path="">Root</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div id="mediaGrid" class="row g-3">
|
||||
<div class="col-12 text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($tab === 'git'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_git.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'config'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_config.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'dashboard'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_dashboard.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'analytics'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_analytics.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'settings'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_settings.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'notes'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_notes.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'themes'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_themes.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'plugins'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_plugins.php'; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ═══ MODALS ════════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- File editor -->
|
||||
<div class="modal fade" id="fileEditModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="fileEditName">Edit file</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<input type="hidden" id="fileEditPid" value="<?= $pid ?>">
|
||||
<input type="hidden" id="fileEditPath">
|
||||
<textarea id="fileEditContent" style="display:none"></textarea>
|
||||
<div id="fileEditCm" style="height:70vh"></div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<span id="fileEditStatus" class="text-muted small me-auto"></span>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="fileEditSave">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Upload -->
|
||||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title">Upload file</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="uploadPid" value="<?= $pid ?>">
|
||||
<input type="hidden" id="uploadAccept" value="any">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Target folder</label>
|
||||
<input type="text" id="uploadFolder" class="form-control form-control-sm font-monospace"
|
||||
placeholder="<?= $isHexo ? 'source/images' : '' ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">File</label>
|
||||
<input type="file" id="uploadFile" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div id="uploadResult" class="d-none">
|
||||
<div class="alert alert-success py-2 small mb-2" id="uploadResultMsg"></div>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="uploadResultUrl" class="form-control font-monospace" readonly>
|
||||
<button class="btn btn-outline-secondary" id="uploadCopy" type="button">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="uploadSubmit">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command history -->
|
||||
<div class="modal fade" id="cmdHistoryModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title">Command history</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div id="cmdHistoryList" class="list-group list-group-flush">
|
||||
<div class="list-group-item text-muted small">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete project -->
|
||||
<div class="modal fade" id="deleteProjectModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title">Remove project?</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body small text-muted">Files on disk are untouched.</div>
|
||||
<div class="modal-footer py-2">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" id="confirmDelete"
|
||||
data-project-id="<?= $pid ?>">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div id="toastContainer" style="position:fixed;bottom:1rem;right:1rem;z-index:9999;min-width:260px"></div>
|
||||
|
||||
<?php
|
||||
$extra_scripts = '
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
|
||||
<script type="module">
|
||||
// Mirrors Agenda\'s Milkdown loader (lib/layout.php). Front matter is NOT fed
|
||||
// through Milkdown — it\'s extracted in JS and edited in a separate CodeMirror
|
||||
// so the YAML round-trips byte-for-byte.
|
||||
const H = "https://esm.sh/@milkdown/";
|
||||
const V = "@7.20.0/es2022/";
|
||||
Promise.all([
|
||||
import(H + "core" + V + "core.mjs"),
|
||||
import(H + "preset-commonmark" + V + "preset-commonmark.mjs"),
|
||||
import(H + "preset-gfm" + V + "preset-gfm.mjs"),
|
||||
import(H + "plugin-history" + V + "plugin-history.mjs"),
|
||||
import(H + "utils" + V + "utils.mjs"),
|
||||
]).then(function ([core, cm, gfmPkg, hist, utils]) {
|
||||
window.MilkdownKit = {
|
||||
Editor: core.Editor, rootCtx: core.rootCtx,
|
||||
defaultValueCtx: core.defaultValueCtx, commandsCtx: core.commandsCtx,
|
||||
commonmark: cm.commonmark, gfm: gfmPkg.gfm, history: hist.history,
|
||||
getMarkdown: utils.getMarkdown, replaceAll: utils.replaceAll,
|
||||
callCommand: utils.callCommand,
|
||||
commands: {
|
||||
bold: cm.toggleStrongCommand, italic: cm.toggleEmphasisCommand,
|
||||
strikethrough: gfmPkg.toggleStrikethroughCommand,
|
||||
inlineCode: cm.toggleInlineCodeCommand, link: cm.toggleLinkCommand,
|
||||
bulletList: cm.wrapInBulletListCommand, orderedList: cm.wrapInOrderedListCommand,
|
||||
blockquote: cm.wrapInBlockquoteCommand, codeBlock: cm.createCodeBlockCommand,
|
||||
},
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent("milkdown-ready"));
|
||||
}).catch(function (err) { console.error("[Milkdown] core load failed:", err); });
|
||||
</script>
|
||||
<script src="/assets/js/milkdown-mount.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/markdown/markdown.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/php/php.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/shell/shell.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||
';
|
||||
include ROOT . '/views/_footer.php';
|
||||
?>
|
||||
84
views/settings.php
Normal file
84
views/settings.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
$page_title = 'Settings';
|
||||
$nav_active = 'settings';
|
||||
include ROOT . '/views/_header.php';
|
||||
|
||||
$scan_paths = $db->query('SELECT * FROM scan_paths ORDER BY path')->fetchAll();
|
||||
?>
|
||||
<h2 class="h4 mb-4">Settings</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Project discovery</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Directories to scan. HackmanCMS detects project types automatically by looking for marker files
|
||||
(<code>_config.yml</code> → Hexo, <code>index.php</code> → Website, etc.).
|
||||
</p>
|
||||
|
||||
<ul class="list-group list-group-flush mb-3" id="scanPathsList">
|
||||
<?php if (empty($scan_paths)): ?>
|
||||
<li class="list-group-item text-muted px-0 small">No scan paths configured yet.</li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($scan_paths as $sp): ?>
|
||||
<li class="list-group-item d-flex align-items-center gap-2 px-0">
|
||||
<code class="flex-grow-1"><?= htmlspecialchars($sp['path']) ?></code>
|
||||
<span class="badge bg-secondary">depth <?= (int)$sp['depth'] ?></span>
|
||||
<button class="btn btn-sm btn-outline-primary btn-scan" data-path="<?= htmlspecialchars($sp['path']) ?>"
|
||||
data-depth="<?= (int)$sp['depth'] ?>">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger btn-remove-scan" data-id="<?= (int)$sp['id'] ?>">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<form id="addScanPathForm" class="d-flex gap-2">
|
||||
<input type="text" name="path" class="form-control form-control-sm" placeholder="/var/www" required>
|
||||
<input type="number" name="depth" class="form-control form-control-sm" value="2" min="1" max="5"
|
||||
style="width:72px" title="Scan depth">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">Project types</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Drop a PHP file extending <code>ProjectTypeBase</code> into
|
||||
<code>lib/project-types/</code> to register a new type — no config needed.
|
||||
</p>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach (ProjectTypes::all() as $slug => $class): ?>
|
||||
<li class="list-group-item px-0 d-flex align-items-center gap-2">
|
||||
<i class="bi <?= htmlspecialchars($class::typeIcon()) ?> text-primary"></i>
|
||||
<span><?= htmlspecialchars($class::typeName()) ?></span>
|
||||
<?php if ($class::description()): ?>
|
||||
<small class="text-muted"><?= htmlspecialchars($class::description()) ?></small>
|
||||
<?php endif; ?>
|
||||
<code class="ms-auto text-muted small"><?= htmlspecialchars($slug) ?></code>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="scanResults" class="mt-4 d-none">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Scan results</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="closeScanResults">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="scanResultsList"></div>
|
||||
</div>
|
||||
|
||||
<?php include ROOT . '/views/_footer.php'; ?>
|
||||
Loading…
Add table
Add a link
Reference in a new issue