Ship migration, deploy workflow, and security hardening updates
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ CLAUDE.md
|
|||||||
/jabali-panel_*.deb
|
/jabali-panel_*.deb
|
||||||
/jabali-deps_*.deb
|
/jabali-deps_*.deb
|
||||||
.git-credentials
|
.git-credentials
|
||||||
|
|
||||||
|
# Local repository configuration (do not commit)
|
||||||
|
config.toml
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class ImportProcessCommand extends Command
|
|||||||
Domain::create([
|
Domain::create([
|
||||||
'domain' => $account->main_domain,
|
'domain' => $account->main_domain,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'document_root' => "/home/{$user->username}/domains/{$account->main_domain}/public",
|
'document_root' => "/home/{$user->username}/domains/{$account->main_domain}/public_html",
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
$account->addLog("Created main domain: {$account->main_domain}");
|
$account->addLog("Created main domain: {$account->main_domain}");
|
||||||
@@ -235,7 +235,7 @@ class ImportProcessCommand extends Command
|
|||||||
Domain::create([
|
Domain::create([
|
||||||
'domain' => $domain,
|
'domain' => $domain,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'document_root' => "/home/{$user->username}/domains/{$domain}/public",
|
'document_root' => "/home/{$user->username}/domains/{$domain}/public_html",
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
$account->addLog("Created addon domain: {$domain}");
|
$account->addLog("Created addon domain: {$domain}");
|
||||||
@@ -290,7 +290,7 @@ class ImportProcessCommand extends Command
|
|||||||
// Copy public_html to the domain
|
// Copy public_html to the domain
|
||||||
$publicHtml = "$homeDir/public_html";
|
$publicHtml = "$homeDir/public_html";
|
||||||
if (is_dir($publicHtml) && $account->main_domain) {
|
if (is_dir($publicHtml) && $account->main_domain) {
|
||||||
$destDir = "/home/{$user->username}/domains/{$account->main_domain}/public";
|
$destDir = "/home/{$user->username}/domains/{$account->main_domain}/public_html";
|
||||||
if (is_dir($destDir)) {
|
if (is_dir($destDir)) {
|
||||||
exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1');
|
exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1');
|
||||||
exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1');
|
exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1');
|
||||||
@@ -316,7 +316,7 @@ class ImportProcessCommand extends Command
|
|||||||
$publicHtml = "$domainDir/public_html";
|
$publicHtml = "$domainDir/public_html";
|
||||||
|
|
||||||
if (is_dir($publicHtml)) {
|
if (is_dir($publicHtml)) {
|
||||||
$destDir = "/home/{$user->username}/domains/{$domain}/public";
|
$destDir = "/home/{$user->username}/domains/{$domain}/public_html";
|
||||||
if (is_dir($destDir)) {
|
if (is_dir($destDir)) {
|
||||||
exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1');
|
exec('cp -r '.escapeshellarg($publicHtml).'/* '.escapeshellarg($destDir).'/ 2>&1');
|
||||||
exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1');
|
exec('chown -R '.escapeshellarg($user->username).':'.escapeshellarg($user->username).' '.escapeshellarg($destDir).' 2>&1');
|
||||||
|
|||||||
@@ -164,28 +164,25 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
*/
|
*/
|
||||||
public function getDiskUsageBytes(): int
|
public function getDiskUsageBytes(): int
|
||||||
{
|
{
|
||||||
// Try to get usage from quota system first (more accurate)
|
// Disk usage must be obtained via the agent (root) to avoid permission-based undercounting.
|
||||||
try {
|
try {
|
||||||
$agent = new \App\Services\Agent\AgentClient;
|
$agent = new \App\Services\Agent\AgentClient(
|
||||||
$result = $agent->quotaGet($this->username, '/');
|
(string) config('jabali.agent.socket', '/var/run/jabali/agent.sock'),
|
||||||
|
(int) config('jabali.agent.timeout', 120),
|
||||||
|
);
|
||||||
|
|
||||||
|
$mount = $this->home_directory ?: ("/home/{$this->username}");
|
||||||
|
$result = $agent->quotaGet($this->username, $mount);
|
||||||
|
|
||||||
if (($result['success'] ?? false) && isset($result['used_mb'])) {
|
if (($result['success'] ?? false) && isset($result['used_mb'])) {
|
||||||
return (int) ($result['used_mb'] * 1024 * 1024);
|
return (int) ($result['used_mb'] * 1024 * 1024);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) {
|
||||||
// Fall back to du command
|
\Log::warning('Disk usage read failed via agent: '.$e->getMessage(), [
|
||||||
|
'username' => $this->username,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
// Fallback: try du command (may not work if www-data can't read home dir)
|
|
||||||
$homeDir = $this->home_directory;
|
|
||||||
|
|
||||||
if (! is_dir($homeDir)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = shell_exec('du -sb '.escapeshellarg($homeDir).' 2>/dev/null | cut -f1');
|
|
||||||
|
|
||||||
return (int) trim($output ?: '0');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
104
bin/jabali-agent
104
bin/jabali-agent
@@ -20681,43 +20681,63 @@ function quotaGet(array $params): array
|
|||||||
{
|
{
|
||||||
$username = $params['username'] ?? '';
|
$username = $params['username'] ?? '';
|
||||||
$mountPoint = $params['mount'] ?? '/home';
|
$mountPoint = $params['mount'] ?? '/home';
|
||||||
|
|
||||||
if (empty($username)) {
|
if (empty($username)) {
|
||||||
return ['success' => false, 'error' => 'Username is required'];
|
return ['success' => false, 'error' => 'Username is required'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Root-level disk usage probe for /home/<user>.
|
||||||
|
$getDuUsageMb = static function (string $user): float {
|
||||||
|
$homeDir = "/home/{$user}";
|
||||||
|
if (!is_dir($homeDir)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$duOutput = trim(shell_exec("du -sk " . escapeshellarg($homeDir) . " 2>/dev/null | cut -f1") ?? '');
|
||||||
|
if ($duOutput === '' || preg_match('/^\d+$/', $duOutput) !== 1) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round(((int) $duOutput) / 1024, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Quota counters can be stale on some filesystems; prefer the larger value.
|
||||||
|
$normalizeUsage = static function (float $usedMb, float $softMb, float $hardMb, float $duMb): array {
|
||||||
|
$finalUsedMb = $duMb > $usedMb ? $duMb : $usedMb;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'used_mb' => $finalUsedMb,
|
||||||
|
'soft_mb' => $softMb,
|
||||||
|
'hard_mb' => $hardMb,
|
||||||
|
'usage_percent' => $hardMb > 0 ? round(($finalUsedMb / $hardMb) * 100, 1) : 0,
|
||||||
|
'quota_source' => $duMb > $usedMb ? 'du_override' : 'quota',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
// Find the actual mount point
|
// Find the actual mount point
|
||||||
$findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
|
$findMount = trim(shell_exec("df --output=target " . escapeshellarg($mountPoint) . " 2>/dev/null | tail -1") ?? '');
|
||||||
if (empty($findMount)) {
|
if (empty($findMount)) {
|
||||||
$findMount = '/';
|
$findMount = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get quota info
|
// Get quota info
|
||||||
$cmd = sprintf('quota -u %s -w 2>&1', escapeshellarg($username));
|
$cmd = sprintf('quota -u %s -w 2>&1', escapeshellarg($username));
|
||||||
exec($cmd, $output, $exitCode);
|
exec($cmd, $output, $exitCode);
|
||||||
|
|
||||||
$outputStr = implode("\n", $output);
|
$outputStr = implode("\n", $output);
|
||||||
|
|
||||||
// Check if user has no quota
|
// Check if user has no quota
|
||||||
if (strpos($outputStr, 'none') !== false || $exitCode !== 0) {
|
if (strpos($outputStr, 'none') !== false || $exitCode !== 0) {
|
||||||
// Try repquota for more detailed info
|
// Try repquota for more detailed info
|
||||||
$cmd2 = sprintf('repquota -u %s 2>/dev/null | grep "^%s"',
|
$cmd2 = sprintf('repquota -u %s 2>/dev/null | grep "^%s"',
|
||||||
escapeshellarg($findMount),
|
escapeshellarg($findMount),
|
||||||
escapeshellarg($username)
|
escapeshellarg($username)
|
||||||
);
|
);
|
||||||
$repOutput = trim(shell_exec($cmd2) ?? '');
|
$repOutput = trim(shell_exec($cmd2) ?? '');
|
||||||
|
|
||||||
if (empty($repOutput)) {
|
|
||||||
// Quota not enabled - use du to calculate actual disk usage
|
|
||||||
$homeDir = "/home/{$username}";
|
|
||||||
$usedMb = 0;
|
|
||||||
|
|
||||||
if (is_dir($homeDir)) {
|
if (empty($repOutput)) {
|
||||||
// Use du to get actual disk usage in KB
|
// Quota not enabled - use du to calculate actual disk usage.
|
||||||
$duOutput = trim(shell_exec("du -sk " . escapeshellarg($homeDir) . " 2>/dev/null | cut -f1") ?? '0');
|
$usedMb = $getDuUsageMb($username);
|
||||||
$usedKb = (int)$duOutput;
|
|
||||||
$usedMb = round($usedKb / 1024, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
@@ -20727,36 +20747,39 @@ function quotaGet(array $params): array
|
|||||||
'soft_mb' => 0,
|
'soft_mb' => 0,
|
||||||
'hard_mb' => 0,
|
'hard_mb' => 0,
|
||||||
'usage_percent' => 0,
|
'usage_percent' => 0,
|
||||||
'quota_source' => 'du' // Indicate that we used du fallback
|
'quota_source' => 'du',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse repquota output
|
// Parse repquota output
|
||||||
// Format: username -- used soft hard grace used soft hard grace
|
// Format: username -- used soft hard grace used soft hard grace
|
||||||
$parts = preg_split('/\s+/', $repOutput);
|
$parts = preg_split('/\s+/', $repOutput);
|
||||||
if (count($parts) >= 5) {
|
if (count($parts) >= 5) {
|
||||||
$usedKb = (int)$parts[2];
|
$usedMb = round(((int) $parts[2]) / 1024, 2);
|
||||||
$softKb = (int)$parts[3];
|
$softMb = round(((int) $parts[3]) / 1024, 2);
|
||||||
$hardKb = (int)$parts[4];
|
$hardMb = round(((int) $parts[4]) / 1024, 2);
|
||||||
|
$duMb = $getDuUsageMb($username);
|
||||||
|
$usage = $normalizeUsage($usedMb, $softMb, $hardMb, $duMb);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'has_quota' => $softKb > 0 || $hardKb > 0,
|
'has_quota' => $softMb > 0 || $hardMb > 0,
|
||||||
'used_mb' => round($usedKb / 1024, 2),
|
'used_mb' => $usage['used_mb'],
|
||||||
'soft_mb' => round($softKb / 1024, 2),
|
'soft_mb' => $usage['soft_mb'],
|
||||||
'hard_mb' => round($hardKb / 1024, 2),
|
'hard_mb' => $usage['hard_mb'],
|
||||||
'usage_percent' => $hardKb > 0 ? round(($usedKb / $hardKb) * 100, 1) : 0
|
'usage_percent' => $usage['usage_percent'],
|
||||||
|
'quota_source' => $usage['quota_source'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse standard quota output
|
// Parse standard quota output
|
||||||
// Look for lines with filesystem info
|
// Look for lines with filesystem info
|
||||||
$usedKb = 0;
|
$usedKb = 0;
|
||||||
$softKb = 0;
|
$softKb = 0;
|
||||||
$hardKb = 0;
|
$hardKb = 0;
|
||||||
|
|
||||||
foreach ($output as $line) {
|
foreach ($output as $line) {
|
||||||
if (preg_match('/^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) {
|
if (preg_match('/^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/', $line, $matches)) {
|
||||||
$usedKb = (int)$matches[2];
|
$usedKb = (int)$matches[2];
|
||||||
@@ -20765,15 +20788,22 @@ function quotaGet(array $params): array
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$usedMb = round($usedKb / 1024, 2);
|
||||||
|
$softMb = round($softKb / 1024, 2);
|
||||||
|
$hardMb = round($hardKb / 1024, 2);
|
||||||
|
$duMb = $getDuUsageMb($username);
|
||||||
|
$usage = $normalizeUsage($usedMb, $softMb, $hardMb, $duMb);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'username' => $username,
|
'username' => $username,
|
||||||
'has_quota' => $softKb > 0 || $hardKb > 0,
|
'has_quota' => $softMb > 0 || $hardMb > 0,
|
||||||
'used_mb' => round($usedKb / 1024, 2),
|
'used_mb' => $usage['used_mb'],
|
||||||
'soft_mb' => round($softKb / 1024, 2),
|
'soft_mb' => $usage['soft_mb'],
|
||||||
'hard_mb' => round($hardKb / 1024, 2),
|
'hard_mb' => $usage['hard_mb'],
|
||||||
'usage_percent' => $hardKb > 0 ? round(($usedKb / $hardKb) * 100, 1) : 0
|
'usage_percent' => $usage['usage_percent'],
|
||||||
|
'quota_source' => $usage['quota_source'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
config.toml.example
Normal file
26
config.toml.example
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Jabali Panel repository config
|
||||||
|
#
|
||||||
|
# Used by `scripts/deploy.sh` (CLI flags still override these settings).
|
||||||
|
# Keep secrets out of this file. Prefer SSH keys and server-side git remotes.
|
||||||
|
|
||||||
|
[deploy]
|
||||||
|
# Test server (where GitHub deploy key is configured)
|
||||||
|
host = "192.168.100.50"
|
||||||
|
user = "root"
|
||||||
|
path = "/var/www/jabali"
|
||||||
|
www_user = "www-data"
|
||||||
|
|
||||||
|
# Optional: keep npm cache outside the repo (saves time on repeated builds)
|
||||||
|
# npm_cache_dir = "/var/www/.npm"
|
||||||
|
|
||||||
|
# Optional: override the branch that gets pushed from the deploy server
|
||||||
|
push_branch = "main"
|
||||||
|
|
||||||
|
# Optional: push to explicit URLs (instead of relying on named remotes)
|
||||||
|
# These pushes run FROM the test server.
|
||||||
|
gitea_url = "ssh://git@192.168.100.100:2222/shukivaknin/jabali-panel.git"
|
||||||
|
github_url = "git@github.com:shukiv/jabali-panel.git"
|
||||||
|
|
||||||
|
# If you prefer named remotes on the deploy server instead of URLs:
|
||||||
|
# gitea_remote = "gitea"
|
||||||
|
# github_remote = "origin"
|
||||||
150
docs/architecture/mcp-and-filament-blueprint.md
Normal file
150
docs/architecture/mcp-and-filament-blueprint.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# MCP and Filament Blueprint (Jabali Panel)
|
||||||
|
|
||||||
|
Last updated: 2026-02-12
|
||||||
|
|
||||||
|
This document is an internal developer blueprint for working on Jabali Panel with:
|
||||||
|
|
||||||
|
- MCP tooling (Model Context Protocol) for fast, version-correct introspection and docs.
|
||||||
|
- Filament (Admin + User panels) conventions and project-specific UI rules.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Keep changes consistent with the existing architecture and UI.
|
||||||
|
- Prefer version-specific documentation and project-aware inspection.
|
||||||
|
- Avoid UI regressions by following Filament-native patterns.
|
||||||
|
- Keep privileged operations isolated behind the agent.
|
||||||
|
|
||||||
|
## MCP Tooling Blueprint
|
||||||
|
|
||||||
|
Jabali is set up to be worked on with MCP tools. Use them to reduce guesswork and prevent version drift.
|
||||||
|
|
||||||
|
### 1) Laravel Boost (Most Important)
|
||||||
|
|
||||||
|
Laravel Boost MCP gives application-aware tools (routes, config, DB schema, logs, and version-specific docs).
|
||||||
|
|
||||||
|
Use it when:
|
||||||
|
- You need to confirm route names/paths and middleware.
|
||||||
|
- You need to confirm the active config (not just what you expect in `.env`).
|
||||||
|
- You need the DB schema or sample records to understand existing behavior.
|
||||||
|
- You need version-specific docs for Laravel/Livewire/Filament/Tailwind.
|
||||||
|
|
||||||
|
Preferred workflow:
|
||||||
|
- `application-info` to confirm versions and installed packages.
|
||||||
|
- `list-routes` to find the correct URL, route names, and panel prefixes.
|
||||||
|
- `get-config` for runtime config values.
|
||||||
|
- `database-schema` and `database-query` (read-only) to verify tables and relationships.
|
||||||
|
- `read-log-entries` / `last-error` to confirm the active failure.
|
||||||
|
- `search-docs` before implementing anything that depends on framework behavior.
|
||||||
|
|
||||||
|
Project rule of thumb:
|
||||||
|
- Before making a structural change in the panel, list relevant routes and key config values first.
|
||||||
|
|
||||||
|
### 2) Jabali Docs MCP Server
|
||||||
|
|
||||||
|
The repository includes `mcp-docs-server/` which exposes project docs as MCP resources/tools.
|
||||||
|
|
||||||
|
What it is useful for:
|
||||||
|
- Quick search across `README.md`, `AGENT.md`, and changelog content.
|
||||||
|
- Pulling a specific section by title.
|
||||||
|
|
||||||
|
This is not a runtime dependency of the panel. It is a developer tooling layer.
|
||||||
|
|
||||||
|
### 3) Frontend and Quality MCPs
|
||||||
|
|
||||||
|
Use these to audit and reduce UI/HTML/CSS regressions:
|
||||||
|
|
||||||
|
- `css-mcp`:
|
||||||
|
- Analyze CSS quality/complexity.
|
||||||
|
- Check browser compatibility for specific CSS features.
|
||||||
|
- Pull MDN docs for CSS properties/selectors when implementing UI.
|
||||||
|
- `stylelint`:
|
||||||
|
- Lint CSS where applicable (note: Filament pages should not use custom CSS files).
|
||||||
|
- `webdev-tools`:
|
||||||
|
- Prettier formatting for snippets.
|
||||||
|
- `php -l` lint for PHP syntax.
|
||||||
|
- HTML validation for standalone HTML.
|
||||||
|
|
||||||
|
Security rule:
|
||||||
|
- Do not send secrets (tokens, passwords, private keys) into any tool query.
|
||||||
|
|
||||||
|
## Filament Blueprint (How Jabali Panels Are Built)
|
||||||
|
|
||||||
|
Jabali has two Filament panels:
|
||||||
|
|
||||||
|
- Admin panel: server-wide operations.
|
||||||
|
- User panel ("Jabali" panel): tenant/user operations.
|
||||||
|
|
||||||
|
High-level structure:
|
||||||
|
- `app/Filament/Admin/*` for admin.
|
||||||
|
- `app/Filament/Jabali/*` for user.
|
||||||
|
|
||||||
|
### Pages vs Resources
|
||||||
|
|
||||||
|
Default decision:
|
||||||
|
- Use a Filament Resource when the UI is primarily CRUD around an Eloquent model.
|
||||||
|
- Use a Filament Page when the UI is a dashboard, a multi-step wizard, or merges multiple concerns into a single screen.
|
||||||
|
|
||||||
|
### Project UI Rules (Strict)
|
||||||
|
|
||||||
|
These rules exist to keep the UI consistent and maintainable:
|
||||||
|
|
||||||
|
- Use Filament native components for layout and UI.
|
||||||
|
- Avoid raw HTML layout in Filament pages.
|
||||||
|
- Avoid custom CSS for Filament pages.
|
||||||
|
- Use Filament tables for list data.
|
||||||
|
|
||||||
|
Practical mapping:
|
||||||
|
- Layout: `Filament\Schemas\Components\Section`, `Grid`, `Tabs`, `Group`.
|
||||||
|
- Actions: `Filament\Actions\Action`.
|
||||||
|
- List data: `HasTable` / `InteractsWithTable` or `EmbeddedTable`.
|
||||||
|
|
||||||
|
### Tabs + Tables Gotcha
|
||||||
|
|
||||||
|
There is a known class of issues when a table is nested incorrectly inside schema Tabs.
|
||||||
|
|
||||||
|
Rule of thumb:
|
||||||
|
- Prefer `EmbeddedTable::make()` in schema layouts.
|
||||||
|
- Avoid mounting tables inside `View::make()` within `Tabs::make()` unless you know the action mounting behavior is preserved.
|
||||||
|
|
||||||
|
### Translations and RTL
|
||||||
|
|
||||||
|
Jabali uses JSON-based translations.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Use the English string as the translation key: `__('Create Domain')`.
|
||||||
|
- Do not introduce dotted translation keys like `__('domain.create')`.
|
||||||
|
- Ensure UI reads correctly in RTL locales (Arabic/Hebrew).
|
||||||
|
|
||||||
|
### Privileged Operations (Agent Boundary)
|
||||||
|
|
||||||
|
The Laravel app is the control plane. Privileged system operations are executed by the root-level agent.
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- The agent is `bin/jabali-agent`.
|
||||||
|
- The panel should call privileged operations through the Agent client service (not by shelling out directly).
|
||||||
|
- Keep all path and input validation strict before an agent call.
|
||||||
|
|
||||||
|
## Filament Blueprint Planning (Feature Specs)
|
||||||
|
|
||||||
|
When writing an implementation plan for a Filament feature, use Filament Blueprint planning docs as a checklist.
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
- `vendor/filament/blueprint/resources/markdown/planning/overview.md`
|
||||||
|
|
||||||
|
At minimum, a plan should specify:
|
||||||
|
- Data model changes (tables, columns, indexes, relationships).
|
||||||
|
- Panel placement (Admin vs User) and navigation.
|
||||||
|
- Page/Resource decisions.
|
||||||
|
- Authorization model (policies/guards).
|
||||||
|
- Background jobs (for long-running operations).
|
||||||
|
- Audit logging events.
|
||||||
|
- Tests (Feature tests for endpoints and Livewire/Filament behaviors).
|
||||||
|
|
||||||
|
## Development Checklist (Per Feature)
|
||||||
|
|
||||||
|
- Confirm the correct panel and route prefix.
|
||||||
|
- List routes and verify config assumptions (Boost tools).
|
||||||
|
- Follow Filament-native components (no custom HTML/CSS in Filament pages).
|
||||||
|
- Use tables for list data.
|
||||||
|
- Keep agent boundary intact for privileged operations.
|
||||||
|
- Add or update tests and run targeted test commands.
|
||||||
@@ -14,6 +14,7 @@ Last updated: 2026-02-10
|
|||||||
- /var/www/jabali/docs/installation.md - Debian package install path, Filament notifications patch, and deploy script usage.
|
- /var/www/jabali/docs/installation.md - Debian package install path, Filament notifications patch, and deploy script usage.
|
||||||
- /var/www/jabali/docs/architecture/control-panel-blueprint.md - High-level blueprint for a hosting panel.
|
- /var/www/jabali/docs/architecture/control-panel-blueprint.md - High-level blueprint for a hosting panel.
|
||||||
- /var/www/jabali/docs/architecture/directadmin-migration-blueprint.md - Blueprint for migrating DirectAdmin accounts into Jabali.
|
- /var/www/jabali/docs/architecture/directadmin-migration-blueprint.md - Blueprint for migrating DirectAdmin accounts into Jabali.
|
||||||
|
- /var/www/jabali/docs/architecture/mcp-and-filament-blueprint.md - Developer blueprint for MCP tooling and Filament panel conventions.
|
||||||
- /var/www/jabali/docs/archive-notes.md - Archived files and restore notes.
|
- /var/www/jabali/docs/archive-notes.md - Archived files and restore notes.
|
||||||
- /var/www/jabali/docs/screenshots/README.md - Screenshot generation instructions.
|
- /var/www/jabali/docs/screenshots/README.md - Screenshot generation instructions.
|
||||||
- /var/www/jabali/docs/docs-summary.md - Project documentation summary (generated).
|
- /var/www/jabali/docs/docs-summary.md - Project documentation summary (generated).
|
||||||
|
|||||||
@@ -117,7 +117,14 @@ project to `root@192.168.100.50:/var/www/jabali`, commits on that server, bumps
|
|||||||
`VERSION`, updates the `install.sh` fallback, and pushes to Git remotes from
|
`VERSION`, updates the `install.sh` fallback, and pushes to Git remotes from
|
||||||
that server. Then it runs composer/npm, migrations, and cache rebuilds.
|
that server. Then it runs composer/npm, migrations, and cache rebuilds.
|
||||||
|
|
||||||
Defaults (override via flags or env vars):
|
Defaults (override via flags, env vars, or config.toml):
|
||||||
|
|
||||||
|
Config file:
|
||||||
|
- `config.toml` (ignored by git) is read automatically if present.
|
||||||
|
- Start from `config.toml.example`.
|
||||||
|
- Set `CONFIG_FILE` to use an alternate TOML file path.
|
||||||
|
- Supported keys are in `[deploy]` (for example: `host`, `user`, `path`, `www_user`, `push_branch`, `gitea_url`, `github_url`).
|
||||||
|
|
||||||
- Host: `192.168.100.50`
|
- Host: `192.168.100.50`
|
||||||
- User: `root`
|
- User: `root`
|
||||||
- Path: `/var/www/jabali`
|
- Path: `/var/www/jabali`
|
||||||
|
|||||||
23
install.sh
23
install.sh
@@ -16,7 +16,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||||
JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")"
|
JABALI_VERSION="$(sed -n 's/^VERSION=//p' "$SCRIPT_DIR/VERSION")"
|
||||||
fi
|
fi
|
||||||
JABALI_VERSION="${JABALI_VERSION:-0.9-rc65}"
|
JABALI_VERSION="${JABALI_VERSION:-0.9-rc66}"
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -414,6 +414,7 @@ install_packages() {
|
|||||||
wget
|
wget
|
||||||
zip
|
zip
|
||||||
unzip
|
unzip
|
||||||
|
cron
|
||||||
htop
|
htop
|
||||||
net-tools
|
net-tools
|
||||||
dnsutils
|
dnsutils
|
||||||
@@ -3020,6 +3021,18 @@ setup_scheduler_cron() {
|
|||||||
mkdir -p "$JABALI_DIR/storage/logs"
|
mkdir -p "$JABALI_DIR/storage/logs"
|
||||||
chown -R www-data:www-data "$JABALI_DIR/storage/logs"
|
chown -R www-data:www-data "$JABALI_DIR/storage/logs"
|
||||||
|
|
||||||
|
# Ensure crontab command is available
|
||||||
|
if ! command -v crontab >/dev/null 2>&1; then
|
||||||
|
warn "crontab command not found, installing cron package..."
|
||||||
|
apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq cron || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v crontab >/dev/null 2>&1; then
|
||||||
|
warn "Unable to configure scheduler: crontab command is still missing"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure cron service is enabled and running
|
# Ensure cron service is enabled and running
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl enable cron >/dev/null 2>&1 || true
|
systemctl enable cron >/dev/null 2>&1 || true
|
||||||
@@ -3030,8 +3043,8 @@ setup_scheduler_cron() {
|
|||||||
CRON_LINE="* * * * * cd $JABALI_DIR && php artisan schedule:run >> /dev/null 2>&1"
|
CRON_LINE="* * * * * cd $JABALI_DIR && php artisan schedule:run >> /dev/null 2>&1"
|
||||||
|
|
||||||
# Add to www-data's crontab (not root) to avoid permission issues with log files
|
# Add to www-data's crontab (not root) to avoid permission issues with log files
|
||||||
if ! sudo -u www-data crontab -l 2>/dev/null | grep -q "artisan schedule:run"; then
|
if ! crontab -u www-data -l 2>/dev/null | grep -q "artisan schedule:run"; then
|
||||||
(sudo -u www-data crontab -l 2>/dev/null; echo "$CRON_LINE") | sudo -u www-data crontab -
|
(crontab -u www-data -l 2>/dev/null; echo "$CRON_LINE") | crontab -u www-data -
|
||||||
log "Laravel scheduler cron job added"
|
log "Laravel scheduler cron job added"
|
||||||
else
|
else
|
||||||
log "Laravel scheduler cron job already exists"
|
log "Laravel scheduler cron job already exists"
|
||||||
@@ -3617,7 +3630,9 @@ uninstall() {
|
|||||||
rm -f /etc/logrotate.d/jabali-users
|
rm -f /etc/logrotate.d/jabali-users
|
||||||
|
|
||||||
# Remove www-data cron jobs (Laravel scheduler)
|
# Remove www-data cron jobs (Laravel scheduler)
|
||||||
crontab -u www-data -r 2>/dev/null || true
|
if command -v crontab >/dev/null 2>&1; then
|
||||||
|
crontab -u www-data -r 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
log "Configuration files cleaned"
|
log "Configuration files cleaned"
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,120 @@ set -euo pipefail
|
|||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
DEPLOY_HOST="${DEPLOY_HOST:-192.168.100.50}"
|
|
||||||
DEPLOY_USER="${DEPLOY_USER:-root}"
|
CONFIG_FILE="${CONFIG_FILE:-$ROOT_DIR/config.toml}"
|
||||||
DEPLOY_PATH="${DEPLOY_PATH:-/var/www/jabali}"
|
|
||||||
WWW_USER="${WWW_USER:-www-data}"
|
# Capture env overrides before we assign defaults so config.toml can sit between
|
||||||
NPM_CACHE_DIR="${NPM_CACHE_DIR:-}"
|
# defaults and environment: CLI > env > config > defaults.
|
||||||
GITEA_REMOTE="${GITEA_REMOTE:-gitea}"
|
ENV_DEPLOY_HOST="${DEPLOY_HOST-}"
|
||||||
GITEA_URL="${GITEA_URL:-}"
|
ENV_DEPLOY_USER="${DEPLOY_USER-}"
|
||||||
GITHUB_REMOTE="${GITHUB_REMOTE:-origin}"
|
ENV_DEPLOY_PATH="${DEPLOY_PATH-}"
|
||||||
GITHUB_URL="${GITHUB_URL:-}"
|
ENV_WWW_USER="${WWW_USER-}"
|
||||||
PUSH_BRANCH="${PUSH_BRANCH:-}"
|
ENV_NPM_CACHE_DIR="${NPM_CACHE_DIR-}"
|
||||||
|
ENV_GITEA_REMOTE="${GITEA_REMOTE-}"
|
||||||
|
ENV_GITEA_URL="${GITEA_URL-}"
|
||||||
|
ENV_GITHUB_REMOTE="${GITHUB_REMOTE-}"
|
||||||
|
ENV_GITHUB_URL="${GITHUB_URL-}"
|
||||||
|
ENV_PUSH_BRANCH="${PUSH_BRANCH-}"
|
||||||
|
|
||||||
|
DEPLOY_HOST="192.168.100.50"
|
||||||
|
DEPLOY_USER="root"
|
||||||
|
DEPLOY_PATH="/var/www/jabali"
|
||||||
|
WWW_USER="www-data"
|
||||||
|
NPM_CACHE_DIR=""
|
||||||
|
GITEA_REMOTE="gitea"
|
||||||
|
GITEA_URL=""
|
||||||
|
GITHUB_REMOTE="origin"
|
||||||
|
GITHUB_URL=""
|
||||||
|
PUSH_BRANCH=""
|
||||||
|
|
||||||
|
trim_ws() {
|
||||||
|
local s="${1:-}"
|
||||||
|
s="$(echo "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
toml_unquote() {
|
||||||
|
local v
|
||||||
|
v="$(trim_ws "${1:-}")"
|
||||||
|
|
||||||
|
# Only accept simple double-quoted strings, booleans, or integers.
|
||||||
|
if [[ ${#v} -ge 2 && "${v:0:1}" == '"' && "${v: -1}" == '"' ]]; then
|
||||||
|
printf '%s' "${v:1:${#v}-2}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$v" =~ ^(true|false)$ ]]; then
|
||||||
|
printf '%s' "$v"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$v" =~ ^-?[0-9]+$ ]]; then
|
||||||
|
printf '%s' "$v"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
load_config_toml() {
|
||||||
|
local file section line key raw value
|
||||||
|
file="$1"
|
||||||
|
section=""
|
||||||
|
|
||||||
|
[[ -f "$file" ]] || return 0
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
# Strip comments and whitespace.
|
||||||
|
line="${line%%#*}"
|
||||||
|
line="$(trim_ws "$line")"
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$line" =~ ^\[([A-Za-z0-9_.-]+)\]$ ]]; then
|
||||||
|
section="${BASH_REMATCH[1]}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$section" == "deploy" ]] || continue
|
||||||
|
|
||||||
|
if [[ "$line" =~ ^([A-Za-z0-9_]+)[[:space:]]*=[[:space:]]*(.+)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
raw="${BASH_REMATCH[2]}"
|
||||||
|
value=""
|
||||||
|
|
||||||
|
if ! value="$(toml_unquote "$raw")"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
host) DEPLOY_HOST="$value" ;;
|
||||||
|
user) DEPLOY_USER="$value" ;;
|
||||||
|
path) DEPLOY_PATH="$value" ;;
|
||||||
|
www_user) WWW_USER="$value" ;;
|
||||||
|
npm_cache_dir) NPM_CACHE_DIR="$value" ;;
|
||||||
|
gitea_remote) GITEA_REMOTE="$value" ;;
|
||||||
|
gitea_url) GITEA_URL="$value" ;;
|
||||||
|
github_remote) GITHUB_REMOTE="$value" ;;
|
||||||
|
github_url) GITHUB_URL="$value" ;;
|
||||||
|
push_branch) PUSH_BRANCH="$value" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done < "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_config_toml "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Apply environment overrides on top of config.
|
||||||
|
if [[ -n "${ENV_DEPLOY_HOST:-}" ]]; then DEPLOY_HOST="$ENV_DEPLOY_HOST"; fi
|
||||||
|
if [[ -n "${ENV_DEPLOY_USER:-}" ]]; then DEPLOY_USER="$ENV_DEPLOY_USER"; fi
|
||||||
|
if [[ -n "${ENV_DEPLOY_PATH:-}" ]]; then DEPLOY_PATH="$ENV_DEPLOY_PATH"; fi
|
||||||
|
if [[ -n "${ENV_WWW_USER:-}" ]]; then WWW_USER="$ENV_WWW_USER"; fi
|
||||||
|
if [[ -n "${ENV_NPM_CACHE_DIR:-}" ]]; then NPM_CACHE_DIR="$ENV_NPM_CACHE_DIR"; fi
|
||||||
|
if [[ -n "${ENV_GITEA_REMOTE:-}" ]]; then GITEA_REMOTE="$ENV_GITEA_REMOTE"; fi
|
||||||
|
if [[ -n "${ENV_GITEA_URL:-}" ]]; then GITEA_URL="$ENV_GITEA_URL"; fi
|
||||||
|
if [[ -n "${ENV_GITHUB_REMOTE:-}" ]]; then GITHUB_REMOTE="$ENV_GITHUB_REMOTE"; fi
|
||||||
|
if [[ -n "${ENV_GITHUB_URL:-}" ]]; then GITHUB_URL="$ENV_GITHUB_URL"; fi
|
||||||
|
if [[ -n "${ENV_PUSH_BRANCH:-}" ]]; then PUSH_BRANCH="$ENV_PUSH_BRANCH"; fi
|
||||||
|
|
||||||
SKIP_SYNC=0
|
SKIP_SYNC=0
|
||||||
SKIP_COMPOSER=0
|
SKIP_COMPOSER=0
|
||||||
@@ -57,7 +161,8 @@ Options:
|
|||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH, WWW_USER, NPM_CACHE_DIR, GITEA_REMOTE, GITEA_URL, GITHUB_REMOTE, GITHUB_URL, PUSH_BRANCH
|
CONFIG_FILE points to a TOML file (default: ./config.toml). The script reads [deploy] keys.
|
||||||
|
CONFIG_FILE, DEPLOY_HOST, DEPLOY_USER, DEPLOY_PATH, WWW_USER, NPM_CACHE_DIR, GITEA_REMOTE, GITEA_URL, GITHUB_REMOTE, GITHUB_URL, PUSH_BRANCH
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user