Add Discord Bot project type with process control, config editor, and log viewer
- DiscordBotProject: new project type (discord-bot), auto-detects from main.py + cogs/ - botcontrol API: start/stop/restart/status via sudo systemctl - botconfig API: atomic read/write of config.json; CRUD for streamers, RSS feeds, Mastodon accounts, and LinkedIn pages; tokens masked in GET responses - botlogs API: tail log file (path via project_settings.bot_log_file) - Tab partials: _tab_bot (process control), _tab_botconfig (structured editor), _tab_logs (log viewer with auto-refresh) - view.php: register bot/botconfig/logs tabs with labels, icons, and groups - app.js: bot control panel, config editor with account/page dropdowns in RSS modal, Mastodon accounts CRUD, LinkedIn pages CRUD; audit labels/icons for all new actions - CLAUDE.md: document Discord Bot integration, sudoers requirement, API actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a2a1f356ba
commit
bf3ae2351d
10 changed files with 1025 additions and 0 deletions
53
views/project/_tab_bot.php
Normal file
53
views/project/_tab_bot.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<div id="botControlPanel" data-project-id="<?= $pid ?>">
|
||||
|
||||
<!-- Status card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body d-flex align-items-center gap-3 flex-wrap">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span id="botStatusDot" class="rounded-circle d-inline-block"
|
||||
style="width:12px;height:12px;background:#6c757d"></span>
|
||||
<span id="botStatusText" class="fw-semibold">Checking…</span>
|
||||
</div>
|
||||
<small id="botSince" class="text-muted"></small>
|
||||
<small id="botService" class="text-muted font-monospace ms-auto"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button class="btn btn-success btn-sm" id="botStartBtn">
|
||||
<i class="bi bi-play-fill me-1"></i>Start
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" id="botStopBtn">
|
||||
<i class="bi bi-stop-fill me-1"></i>Stop
|
||||
</button>
|
||||
<button class="btn btn-warning btn-sm" id="botRestartBtn">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Restart
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm ms-auto" id="botRefreshStatusBtn">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Output -->
|
||||
<div id="botCmdOutputWrap" class="d-none">
|
||||
<div class="card">
|
||||
<div class="card-header small d-flex justify-content-between align-items-center">
|
||||
Command output
|
||||
<button class="btn btn-sm btn-outline-secondary py-0" id="botClearOutputBtn">Clear</button>
|
||||
</div>
|
||||
<pre id="botCmdOutput" class="m-0 p-3 text-success"
|
||||
style="min-height:80px;max-height:300px;overflow-y:auto;font-size:.8rem;background:transparent"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-muted small mb-1">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Bot process managed via <code>systemctl</code>.
|
||||
The <code>www-data</code> user requires passwordless sudo for these commands:
|
||||
</p>
|
||||
<pre class="small text-muted p-2 border rounded bg-body-tertiary" style="font-size:.75rem">www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl start bashybot, /usr/bin/systemctl stop bashybot, /usr/bin/systemctl restart bashybot, /usr/bin/systemctl is-active bashybot, /usr/bin/systemctl show bashybot</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
214
views/project/_tab_botconfig.php
Normal file
214
views/project/_tab_botconfig.php
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<div id="botConfigPanel" data-project-id="<?= $pid ?>">
|
||||
|
||||
<!-- Twitch Streamers -->
|
||||
<h6 class="mb-3"><i class="bi bi-twitch me-1 text-primary"></i>Twitch Streamers</h6>
|
||||
<div id="streamerList" class="mb-2"><div class="text-muted small">Loading…</div></div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="addStreamerBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Streamer
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- RSS Feeds -->
|
||||
<h6 class="mb-3 mt-3"><i class="bi bi-rss me-1 text-warning"></i>RSS Feeds</h6>
|
||||
<div id="rssList" class="mb-2"><div class="text-muted small">Loading…</div></div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="addRssBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Feed
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Mastodon Accounts -->
|
||||
<h6 class="mb-1 mt-3"><i class="bi bi-mastodon me-1 text-primary"></i>Mastodon Accounts</h6>
|
||||
<p class="text-muted small mb-3">
|
||||
Add the token to <code>.env</code> as <code>MASTODON_TOKEN_<NAME></code> (uppercase).
|
||||
The UI manages the label and API base URL only.
|
||||
</p>
|
||||
<div id="mastodonList" class="mb-2"><div class="text-muted small">Loading…</div></div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="addMastodonBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Account
|
||||
</button>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- LinkedIn Pages -->
|
||||
<h6 class="mb-3 mt-3"><i class="bi bi-linkedin me-1 text-primary"></i>LinkedIn Pages</h6>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2 d-flex align-items-center gap-3">
|
||||
<div>
|
||||
<div class="small fw-semibold" id="liConnectionLabel">Checking…</div>
|
||||
<div class="text-muted small" id="liTokenExpiry"></div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary ms-auto disabled" id="liConnectBtn">
|
||||
<i class="bi bi-box-arrow-in-right me-1"></i>Connect LinkedIn
|
||||
<span class="badge bg-secondary ms-1">coming soon</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="linkedinPagesList" class="mb-2"><div class="text-muted small">Loading…</div></div>
|
||||
<button class="btn btn-sm btn-outline-primary mb-4" id="addLinkedinPageBtn">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Page
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Streamer modal -->
|
||||
<div class="modal fade" id="streamerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="streamerModalTitle">Add Streamer</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="streamerModalMode">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Twitch login name</label>
|
||||
<input type="text" id="streamerName" class="form-control form-control-sm font-monospace"
|
||||
placeholder="streamer_login" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Discord channel ID</label>
|
||||
<input type="text" id="streamerChannelId" class="form-control form-control-sm font-monospace">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Role ID <span class="text-muted">(optional)</span></label>
|
||||
<input type="text" id="streamerRoleId" class="form-control form-control-sm font-monospace">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Embed colour</label>
|
||||
<input type="color" id="streamerColor" class="form-control form-control-color form-control-sm" value="#6441a5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<div class="text-danger small d-none flex-grow-1" id="streamerModalError"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="streamerModalSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RSS modal -->
|
||||
<div class="modal fade" id="rssModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="rssModalTitle">Add RSS Feed</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="rssModalMode">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Feed name (label)</label>
|
||||
<input type="text" id="rssName" class="form-control form-control-sm font-monospace"
|
||||
placeholder="myblog" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">RSS URL</label>
|
||||
<input type="url" id="rssUrl" class="form-control form-control-sm font-monospace">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Discord channel ID</label>
|
||||
<input type="text" id="rssChannelId" class="form-control form-control-sm font-monospace">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Role ID <span class="text-muted">(optional)</span></label>
|
||||
<input type="text" id="rssRoleId" class="form-control form-control-sm font-monospace">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Embed colour</label>
|
||||
<input type="color" id="rssColor" class="form-control form-control-color form-control-sm" value="#ffd700">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Mastodon account <span class="text-muted">(optional)</span></label>
|
||||
<select id="rssMastodonAccount" class="form-select form-select-sm"></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">LinkedIn page <span class="text-muted">(optional)</span></label>
|
||||
<select id="rssLinkedinPage" class="form-select form-select-sm"></select>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="rssActive" checked>
|
||||
<label class="form-check-label small" for="rssActive">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<div class="text-danger small d-none flex-grow-1" id="rssModalError"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="rssModalSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mastodon account modal -->
|
||||
<div class="modal fade" id="mastodonModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="mastodonModalTitle">Add Mastodon Account</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="mastodonModalMode">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Account key</label>
|
||||
<input type="text" id="mastodonName" class="form-control form-control-sm font-monospace"
|
||||
placeholder="main" autocomplete="off">
|
||||
<div class="form-text small">Used as <code>MASTODON_TOKEN_<KEY></code> in .env (uppercase).</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Label</label>
|
||||
<input type="text" id="mastodonLabel" class="form-control form-control-sm" placeholder="Main Account">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">API base URL</label>
|
||||
<input type="url" id="mastodonBase" class="form-control form-control-sm font-monospace"
|
||||
placeholder="https://mastodon.social">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<div class="text-danger small d-none flex-grow-1" id="mastodonModalError"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="mastodonModalSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn page modal -->
|
||||
<div class="modal fade" id="linkedinPageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header py-2">
|
||||
<h6 class="modal-title" id="linkedinPageModalTitle">Add LinkedIn Page</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="linkedinPageModalMode">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Page key</label>
|
||||
<input type="text" id="linkedinPageName" class="form-control form-control-sm font-monospace"
|
||||
placeholder="myorg" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Label</label>
|
||||
<input type="text" id="linkedinPageLabel" class="form-control form-control-sm" placeholder="My Organisation">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Organization ID</label>
|
||||
<input type="text" id="linkedinPageOrgId" class="form-control form-control-sm font-monospace"
|
||||
placeholder="123456789">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2">
|
||||
<div class="text-danger small d-none flex-grow-1" id="linkedinPageModalError"></div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="linkedinPageModalSaveBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
28
views/project/_tab_logs.php
Normal file
28
views/project/_tab_logs.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<div id="botLogsPanel" data-project-id="<?= $pid ?>">
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-3 flex-wrap">
|
||||
<h6 class="mb-0 text-muted">
|
||||
<i class="bi bi-journal-text me-1"></i>Bot logs
|
||||
</h6>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<label class="form-label small mb-0">Lines:</label>
|
||||
<select id="logsLineCount" class="form-select form-select-sm" style="width:auto">
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
<option value="500">500</option>
|
||||
</select>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="logsAutoRefresh">
|
||||
<label class="form-check-label small" for="logsAutoRefresh">Auto-refresh</label>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="logsRefreshBtn">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre id="logsOutput" class="p-3 border rounded bg-body-tertiary text-body-secondary"
|
||||
style="font-size:.75rem;min-height:200px;max-height:70vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all">Loading…</pre>
|
||||
|
||||
</div>
|
||||
|
|
@ -39,6 +39,8 @@ $tabLabels = [
|
|||
'run' => 'Run', 'themes' => 'Themes',
|
||||
'plugins' => 'Plugins', 'git' => 'Git',
|
||||
'notes' => 'Notes', 'settings' => 'Settings',
|
||||
'bot' => 'Bot', 'botconfig' => 'Config',
|
||||
'logs' => 'Logs',
|
||||
];
|
||||
$tabIcons = [
|
||||
'dashboard' => 'bi-grid-1x2', 'analytics' => 'bi-graph-up',
|
||||
|
|
@ -47,9 +49,12 @@ $tabIcons = [
|
|||
'run' => 'bi-terminal', 'themes' => 'bi-palette',
|
||||
'plugins' => 'bi-puzzle', 'git' => 'bi-git',
|
||||
'notes' => 'bi-sticky', 'settings' => 'bi-gear',
|
||||
'bot' => 'bi-robot', 'botconfig' => 'bi-sliders2',
|
||||
'logs' => 'bi-journal-text',
|
||||
];
|
||||
$tabGroups = [
|
||||
['dashboard', 'analytics'],
|
||||
['bot', 'botconfig', 'logs'],
|
||||
['posts', 'config', 'files', 'media'],
|
||||
['run', 'themes', 'plugins', 'git'],
|
||||
['notes', 'settings'],
|
||||
|
|
@ -256,6 +261,15 @@ $tabGroups = [
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php elseif ($tab === 'bot'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_bot.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'botconfig'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_botconfig.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'logs'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_logs.php'; ?>
|
||||
|
||||
<?php elseif ($tab === 'git'): ?>
|
||||
<?php include ROOT . '/views/project/_tab_git.php'; ?>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue