mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-02-07 16:02:10 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
967d68b14a | ||
|
|
b7614aeab7 | ||
|
|
d931784935 | ||
|
|
4755065229 | ||
|
|
e86bbf7681 | ||
|
|
be136eb916 |
@@ -60,6 +60,7 @@ Icons use [Lucide](https://lucide.dev/). Add new icons as macros in `web/templat
|
||||
## Pull Requests
|
||||
|
||||
- Never include unchecked checklists (e.g., `- [ ] ...`) in PR descriptions. Either omit the checklist or use checked items.
|
||||
- **NEVER run `gh pr merge`**. PRs are merged via the GitHub UI, not the CLI.
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/* Sidebar inputs - remove focus outline (DaisyUI 5 uses outline + outline-offset) */
|
||||
#sidebar .input:focus,
|
||||
#sidebar .input:focus-within,
|
||||
#sidebar .select:focus {
|
||||
outline: none;
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
/* Editors (Monaco) - wrapper makes it resizable */
|
||||
.editor-wrapper {
|
||||
resize: vertical;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "partials/components.html" import page_header %}
|
||||
{% from "partials/icons.html" import terminal, save %}
|
||||
{% from "partials/components.html" import page_header, collapse %}
|
||||
{% from "partials/icons.html" import terminal, file_code, save %}
|
||||
{% block title %}Console - Compose Farm{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -20,19 +20,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Terminal -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold flex items-center gap-2">{{ terminal() }} Terminal</h3>
|
||||
<span class="text-xs opacity-50">Full shell access to selected host</span>
|
||||
</div>
|
||||
{% call collapse("Terminal", checked=True, icon=terminal(), subtitle="Full shell access to selected host") %}
|
||||
<div id="console-terminal" class="w-full bg-base-300 rounded-lg overflow-hidden resize-y" style="height: 384px; min-height: 200px;"></div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
<!-- Editor -->
|
||||
<div class="mb-6">
|
||||
{% call collapse("Editor", checked=True, icon=file_code()) %}
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="font-semibold">Editor</h3>
|
||||
<input type="text" id="console-file-path" class="input input-sm input-bordered w-96" placeholder="Enter file path (e.g., ~/docker-compose.yaml)" value="{{ config_path }}">
|
||||
<button class="btn btn-sm btn-outline" onclick="loadFile()">Open</button>
|
||||
</div>
|
||||
@@ -42,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="console-editor" class="resize-y overflow-hidden rounded-lg" style="height: 512px; min-height: 200px;"></div>
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -53,6 +48,13 @@ var consoleEditor = null;
|
||||
var currentFilePath = null;
|
||||
var currentHost = null;
|
||||
|
||||
// Helper to show status with monospace path
|
||||
function setEditorStatus(prefix, path) {
|
||||
const statusEl = document.getElementById('editor-status');
|
||||
const escaped = path.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
statusEl.innerHTML = `${prefix} <code class="font-mono">${escaped}</code>`;
|
||||
}
|
||||
|
||||
function connectConsole() {
|
||||
const hostSelect = document.getElementById('console-host-select');
|
||||
const host = hostSelect.value;
|
||||
@@ -155,7 +157,7 @@ async function loadFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.textContent = `Loading ${path}...`;
|
||||
setEditorStatus('Loading', path + '...');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/console/file?host=${encodeURIComponent(currentHost)}&path=${encodeURIComponent(path)}`);
|
||||
@@ -172,7 +174,7 @@ async function loadFile() {
|
||||
consoleEditor.setValue(data.content);
|
||||
monaco.editor.setModelLanguage(consoleEditor.getModel(), language);
|
||||
currentFilePath = path; // Only set after content is loaded
|
||||
statusEl.textContent = `Loaded: ${path}`;
|
||||
setEditorStatus('Loaded:', path);
|
||||
} else {
|
||||
statusEl.textContent = 'Editor not ready';
|
||||
}
|
||||
@@ -199,7 +201,7 @@ async function saveFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.textContent = `Saving ${currentFilePath}...`;
|
||||
setEditorStatus('Saving', currentFilePath + '...');
|
||||
|
||||
try {
|
||||
const content = consoleEditor.getValue();
|
||||
@@ -215,7 +217,7 @@ async function saveFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.textContent = `Saved: ${currentFilePath}`;
|
||||
setEditorStatus('Saved:', currentFilePath);
|
||||
} catch (e) {
|
||||
statusEl.textContent = `Error: ${e.message}`;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
{% endmacro %}
|
||||
|
||||
{# Collapsible section #}
|
||||
{% macro collapse(title, id=None, checked=False, badge=None, icon=None) %}
|
||||
{% macro collapse(title, id=None, checked=False, badge=None, icon=None, subtitle=None) %}
|
||||
<div class="collapse collapse-arrow bg-base-100 shadow mb-4">
|
||||
<input type="checkbox" {% if id %}id="{{ id }}"{% endif %} {% if checked %}checked{% endif %} />
|
||||
<div class="collapse-title font-medium flex items-center gap-2">
|
||||
<div class="collapse-title font-semibold flex items-center gap-2">
|
||||
{% if icon %}{{ icon }}{% endif %}{{ title }}
|
||||
{% if badge %}<code class="text-xs ml-2 opacity-60">{{ badge }}</code>{% endif %}
|
||||
{% if subtitle %}<span class="text-xs opacity-50 font-normal">{{ subtitle }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
{{ caller() }}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
<div class="mb-4">
|
||||
<h4 class="text-xs uppercase tracking-wide text-base-content/60 px-3 py-1">Services <span class="opacity-50" id="sidebar-count">({{ services | length }})</span></h4>
|
||||
<div class="px-2 mb-2 flex flex-col gap-1">
|
||||
<label class="input input-xs input-bordered flex items-center gap-2 bg-base-200">
|
||||
<label class="input input-xs flex items-center gap-2 bg-base-200">
|
||||
{{ search(14) }}<input type="text" id="sidebar-filter" placeholder="Filter..." onkeyup="sidebarFilter()" />
|
||||
</label>
|
||||
<select id="sidebar-host-select" class="select select-xs select-bordered bg-base-200 w-full" onchange="sidebarFilter()">
|
||||
<select id="sidebar-host-select" class="select select-xs bg-base-200 w-full" onchange="sidebarFilter()">
|
||||
<option value="">All hosts</option>
|
||||
{% for h in hosts %}<option value="{{ h }}">{{ h }}</option>{% endfor %}
|
||||
</select>
|
||||
|
||||
@@ -261,7 +261,9 @@ async def terminal_websocket(websocket: WebSocket, task_id: str) -> None:
|
||||
await websocket.accept()
|
||||
|
||||
if task_id not in tasks:
|
||||
await websocket.send_text(f"{RED}Error: Task not found{RESET}{CRLF}")
|
||||
await websocket.send_text(
|
||||
f"{DIM}Task not found (expired or container restarted).{RESET}{CRLF}"
|
||||
)
|
||||
await websocket.close(code=4004)
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user