mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 03:06:54 +00:00
feat(stack-management): add scan stacks folder button (#332)
* feat(stack-management): add scan stacks folder button to detect manually-placed compose files Users who place Docker Compose files directly into the stacks directory (via SCP, file manager, etc.) can now click the folder-search icon next to "Create Stack" to immediately discover and surface those stacks in the sidebar without a full page reload. * docs(troubleshooting): add scan stacks folder troubleshooting section Covers common issues: compose file not in subdirectory, unrecognized filenames, empty directories, and already-tracked stacks.
This commit is contained in:
@@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
* **stack-management:** scan stacks folder button to detect and import manually-placed compose files
|
||||
|
||||
### Docs
|
||||
|
||||
* **quickstart:** fix Cyrillic character in Docker image reference and correct registry from GHCR to Docker Hub (`saelix/sencho`)
|
||||
|
||||
@@ -75,6 +75,24 @@ Right-click or use the **⋮** button on any stack in the sidebar to access:
|
||||
- **Update** - pull latest images and recreate containers
|
||||
- **Delete** - stop and remove the stack (admin only)
|
||||
|
||||
## Scanning for stacks
|
||||
|
||||
If you place Docker Compose files directly into the stacks directory (for example, via `scp`, a file manager, or the command line), you can import them without going through the full "Create Stack" flow.
|
||||
|
||||
Click the **folder-search icon** next to the "Create Stack" button in the sidebar. Sencho scans the configured stacks directory and reports what it finds:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/stack-scanning/scan-button-sidebar.png" alt="Scan stacks folder button in the sidebar" />
|
||||
</Frame>
|
||||
|
||||
- **New stacks detected** — a success notification lists the newly discovered stack names, and they appear immediately in the sidebar
|
||||
- **Stacks removed from disk** — an informational notification lists stacks that are no longer present in the directory
|
||||
- **No changes** — a notification confirms no new stacks were found
|
||||
|
||||
<Tip>
|
||||
Any subdirectory inside your `COMPOSE_DIR` that contains a `compose.yaml`, `compose.yml`, `docker-compose.yaml`, or `docker-compose.yml` file is recognized as a valid stack.
|
||||
</Tip>
|
||||
|
||||
## Converting a `docker run` command
|
||||
|
||||
If you have an existing `docker run` command and want to turn it into a Compose stack, go to the **Home** tab and paste it into the "Convert Docker Run to Compose" field. Sencho converts it to YAML that you can save as a new stack.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -159,6 +159,28 @@ DELETE FROM global_settings WHERE key IN ('auth_username', 'auth_password_hash',
|
||||
|
||||
---
|
||||
|
||||
## Scan stacks folder doesn't find my stacks
|
||||
|
||||
**Symptom:** You placed Docker Compose files in the stacks directory and clicked the scan button, but "No new stacks found" appears.
|
||||
|
||||
**Checks in order:**
|
||||
|
||||
1. **Is the compose file in a subdirectory?** Sencho only discovers stacks inside subdirectories of `COMPOSE_DIR`. A loose `compose.yaml` sitting directly in the root of `COMPOSE_DIR` is ignored — each stack must be in its own folder (e.g. `COMPOSE_DIR/my-app/compose.yaml`).
|
||||
|
||||
2. **Is the compose file named correctly?** Sencho recognizes these filenames only:
|
||||
- `compose.yaml`
|
||||
- `compose.yml`
|
||||
- `docker-compose.yaml`
|
||||
- `docker-compose.yml`
|
||||
|
||||
Other names (e.g. `docker-compose.prod.yaml`, `stack.yaml`) are not detected.
|
||||
|
||||
3. **Is the directory empty or missing a compose file?** A subdirectory that exists but contains no recognized compose file will not appear. Add a valid compose file to the directory.
|
||||
|
||||
4. **Is the stack already tracked?** Stacks that already appear in the sidebar are not reported again. The scan only reports *changes* since the last time the list was loaded.
|
||||
|
||||
---
|
||||
|
||||
## Checking the health endpoint
|
||||
|
||||
Sencho exposes a health endpoint for monitoring and container health checks:
|
||||
|
||||
@@ -18,7 +18,7 @@ import { springs } from '@/lib/motion';
|
||||
import { Highlight, HighlightItem } from './animate-ui/primitives/effects/highlight';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Plus, Trash2, Play, Square, Save, Terminal, RotateCw, CloudDownload, Pencil, X, Home, ExternalLink, Bell, MoreVertical, BellRing, Rocket, HardDrive, ScrollText, Activity, Server, Radar, Undo2, RefreshCw, Download, Clock, Menu } from 'lucide-react';
|
||||
import { Plus, Trash2, Play, Square, Save, Terminal, RotateCw, CloudDownload, Pencil, X, Home, ExternalLink, Bell, MoreVertical, BellRing, Rocket, HardDrive, ScrollText, Activity, Server, Radar, Undo2, RefreshCw, Download, Clock, Menu, FolderSearch, Loader2 } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { UserProfileDropdown } from './UserProfileDropdown';
|
||||
import { apiFetch, fetchForNode } from '@/lib/api';
|
||||
@@ -116,6 +116,7 @@ export default function EditorLayout() {
|
||||
const [stackToDelete, setStackToDelete] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingAction, setLoadingAction] = useState<string | null>(null);
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||
const [backupInfo, setBackupInfo] = useState<{ exists: boolean; timestamp: number | null }>({ exists: false, timestamp: null });
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
@@ -239,13 +240,13 @@ export default function EditorLayout() {
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [activeTab]);
|
||||
|
||||
const refreshStacks = async (background = false) => {
|
||||
const refreshStacks = async (background = false): Promise<string[]> => {
|
||||
if (!background) setIsLoading(true);
|
||||
try {
|
||||
const res = await apiFetch('/stacks');
|
||||
if (!res.ok) {
|
||||
setFiles([]);
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
const data = await res.json();
|
||||
const fileList: string[] = Array.isArray(data) ? data : [];
|
||||
@@ -267,14 +268,43 @@ export default function EditorLayout() {
|
||||
}
|
||||
}
|
||||
setStackStatuses(statuses);
|
||||
return fileList;
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh stacks:', error);
|
||||
setFiles([]);
|
||||
return [];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScanStacks = async () => {
|
||||
if (isScanning) return;
|
||||
setIsScanning(true);
|
||||
const previousStacks = [...files];
|
||||
try {
|
||||
const currentStacks = await refreshStacks();
|
||||
const added = currentStacks.filter(s => !previousStacks.includes(s));
|
||||
const removed = previousStacks.filter(s => !currentStacks.includes(s));
|
||||
|
||||
if (added.length > 0) {
|
||||
toast.success(`Found ${added.length} new stack${added.length !== 1 ? 's' : ''}: ${added.join(', ')}`);
|
||||
}
|
||||
if (removed.length > 0) {
|
||||
toast.info(`${removed.length} stack${removed.length !== 1 ? 's' : ''} no longer detected: ${removed.join(', ')}`);
|
||||
}
|
||||
if (added.length === 0 && removed.length === 0) {
|
||||
toast.info('No new stacks found.');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Record<string, unknown>;
|
||||
const data = err?.data as Record<string, unknown> | undefined;
|
||||
toast.error((err?.message as string) || (err?.error as string) || (data?.error as string) || 'Something went wrong.');
|
||||
} finally {
|
||||
setIsScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Notification WS push - subscribe to local real-time alerts.
|
||||
// Initial history load is handled by the [nodes] effect below.
|
||||
useEffect(() => {
|
||||
@@ -1163,11 +1193,11 @@ export default function EditorLayout() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Stack Button */}
|
||||
{can('stack:create') && <div className="p-4">
|
||||
{/* Create Stack & Scan Buttons */}
|
||||
{can('stack:create') && <div className="p-4 flex gap-2">
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full rounded-lg">
|
||||
<Button variant="outline" className="flex-1 rounded-lg">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Stack
|
||||
</Button>
|
||||
@@ -1190,6 +1220,26 @@ export default function EditorLayout() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-lg shrink-0"
|
||||
onClick={handleScanStacks}
|
||||
disabled={isScanning}
|
||||
>
|
||||
{isScanning
|
||||
? <Loader2 className="w-4 h-4 animate-spin" strokeWidth={1.5} />
|
||||
: <FolderSearch className="w-4 h-4" strokeWidth={1.5} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Scan stacks folder</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>}
|
||||
|
||||
{/* Search Input & Stack List */}
|
||||
|
||||
Reference in New Issue
Block a user