diff --git a/app/routes/apps.py b/app/routes/apps.py
index ff3ae65..7b183d6 100644
--- a/app/routes/apps.py
+++ b/app/routes/apps.py
@@ -45,6 +45,28 @@ def render_health_status(status: str | None) -> str:
return f'{html.escape(labels.get(normalized, value or "neznámá"))}'
+def render_health_dot(status: str | None, checked_at: str | None) -> str:
+ normalized = (status or "").lower()
+ labels = {
+ "healthy": "zdravá",
+ "unhealthy": "nezdravá",
+ "unreachable": "nedostupná",
+ }
+ class_name = "health-dot health-dot-muted"
+ if normalized == "healthy":
+ class_name = "health-dot health-dot-success"
+ elif normalized == "unhealthy":
+ class_name = "health-dot health-dot-warning"
+ elif normalized == "unreachable":
+ class_name = "health-dot health-dot-danger"
+
+ label = labels.get(normalized, "zdraví neznámé")
+ title = label
+ if checked_at:
+ title = f"{label}, poslední kontrola: {checked_at}"
+ return f''
+
+
@router.get("/")
def portal_home(user=Depends(require_user)):
return RedirectResponse(url="/portal/operations", status_code=303)
@@ -94,67 +116,63 @@ def apps_page(
rows = ""
- for item in apps:
+ for index, item in enumerate(apps, start=1):
app_id = html.escape(item.get("id", ""))
app_url_id = quote(item.get("id", ""), safe="")
+ resource_modal_id = f"resources-{page_number}-{index}"
status = html.escape(item.get("status", ""))
health = latest_health.get(item.get("id", "")) or {}
- health_status = render_health_status(health.get("status"))
health_checked_at = html.escape(health.get("checked_at", "") or "")
+ health_dot = render_health_dot(health.get("status"), health.get("checked_at"))
docs = html.escape(item.get("docs", f"/apps/{app_id}/docs"))
- memory = item.get("memory", "")
- cpus = item.get("cpus", "")
+ memory = html.escape(item.get("memory", "") or "")
+ cpus = html.escape(item.get("cpus", "") or "")
+ memory_label = memory or "výchozí"
+ cpus_label = cpus or "výchozí"
http_clone = html.escape(f"git clone {gitea_url}/{gitea_org}/{app_id}.git")
ssh_clone = html.escape(f"git clone ssh://git@{host}:2222/{gitea_org}/{app_id}.git")
- memory_options = ""
- for value, label in [
- ("", "Výchozí"),
- ("256m", "256 MB - malá služba"),
- ("512m", "512 MB - běžná služba"),
- ("1g", "1 GB - větší služba"),
- ("2g", "2 GB - náročná služba"),
- ]:
- selected = "selected" if memory == value else ""
- memory_options += f''
-
- cpu_options = ""
- for value, label in [
- ("", "Výchozí"),
- ("0.25", "0,25 CPU - velmi malá služba"),
- ("0.50", "0,50 CPU - běžná služba"),
- ("1.00", "1 CPU - celé jádro"),
- ("2.00", "2 CPU - náročná služba"),
- ]:
- selected = "selected" if cpus == value else ""
- cpu_options += f''
-
rows += f"""
-
-
- {app_id}
- /apps/{app_id}
+ |
+
+
+ {health_dot}
+
+ {app_id}
+ /apps/{app_id}
+
+
|
{status} |
- {health_status} {health_checked_at} |
- Swagger |
+ Swagger |
-
+
+ Paměť: {memory_label}
+ CPU: {cpus_label}
+
+
+
|
-
+
Příkazy pro klonování
@@ -169,22 +187,27 @@ def apps_page(
|
-
- Detail
- Nasazení
-
-
+ |
+ Detail
+ Nasazení
+
|
"""
if not rows:
- rows = '| Zatím nejsou nasazené žádné služby. |
'
+ rows = '| Zatím nejsou nasazené žádné služby. |
'
status_options = ['']
for value in status_values:
selected = " selected" if selected_status == value else ""
@@ -265,9 +288,8 @@ def apps_page(
| Služba |
Status |
- Zdraví |
Dokumentace |
- Prostředky |
+ Prostředky i |
Git |
Akce |
diff --git a/app/static/portal.js b/app/static/portal.js
index 1c6f749..247d88c 100644
--- a/app/static/portal.js
+++ b/app/static/portal.js
@@ -82,3 +82,38 @@ function copyText(button) {
.catch(() => showCopied(button, false));
return false;
}
+
+function openResourceModal(id) {
+ const modal = document.getElementById(id);
+ if (!modal) {
+ return false;
+ }
+
+ if (typeof modal.showModal === "function") {
+ modal.showModal();
+ } else {
+ modal.setAttribute("open", "");
+ }
+ return false;
+}
+
+function closeResourceModal(id) {
+ const modal = document.getElementById(id);
+ if (!modal) {
+ return false;
+ }
+
+ if (typeof modal.close === "function") {
+ modal.close();
+ } else {
+ modal.removeAttribute("open");
+ }
+ return false;
+}
+
+document.addEventListener("click", (event) => {
+ const modal = event.target;
+ if (typeof HTMLDialogElement !== "undefined" && modal instanceof HTMLDialogElement && modal.classList.contains("resource-modal")) {
+ modal.close();
+ }
+});
diff --git a/app/static/styles.css b/app/static/styles.css
index f250b0a..28cb2b1 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -397,6 +397,51 @@ button:hover,
color: var(--muted);
}
+.health-dot {
+ display: inline-block;
+ width: 11px;
+ height: 11px;
+ border-radius: 999px;
+ background: var(--muted);
+ box-shadow: 0 0 0 4px rgba(98, 121, 135, 0.14);
+ flex: 0 0 auto;
+}
+
+.health-dot-success {
+ background: var(--success);
+ box-shadow: 0 0 0 4px var(--success-bg);
+}
+
+.health-dot-warning {
+ background: var(--warning);
+ box-shadow: 0 0 0 4px var(--warning-bg);
+}
+
+.health-dot-danger {
+ background: var(--danger);
+ box-shadow: 0 0 0 4px var(--danger-bg);
+}
+
+.health-dot-muted {
+ background: var(--muted);
+}
+
+.info-dot {
+ display: inline-flex;
+ width: 18px;
+ height: 18px;
+ margin-left: 4px;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--muted);
+ border-radius: 999px;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 1;
+ text-transform: none;
+}
+
input,
select {
padding: 8px;
@@ -447,6 +492,192 @@ input[readonly] {
margin: 0 0 8px;
}
+.service-row td {
+ vertical-align: middle;
+}
+
+.service-name-cell {
+ min-width: 260px;
+}
+
+.service-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.resource-summary {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 5px;
+ min-width: 170px;
+}
+
+.resource-summary span {
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.resource-summary strong {
+ color: var(--text);
+}
+
+.btn-compact {
+ width: fit-content;
+ min-height: 30px;
+ padding: 6px 10px;
+ font-size: 13px;
+}
+
+.resource-modal {
+ width: min(520px, calc(100vw - 32px));
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 20px;
+ color: var(--text);
+ box-shadow: 0 18px 48px rgba(10, 31, 42, 0.26);
+}
+
+.resource-modal::backdrop {
+ background: rgba(10, 31, 42, 0.34);
+}
+
+.resource-modal form {
+ display: grid;
+ gap: 10px;
+ margin: 0;
+}
+
+.resource-modal h3 {
+ margin: 0;
+ color: var(--secondary);
+}
+
+.resource-modal label {
+ font-weight: 800;
+ color: var(--secondary);
+}
+
+.resource-modal input {
+ width: 100%;
+}
+
+.modal-header,
+.modal-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.modal-actions {
+ justify-content: flex-end;
+ margin-top: 8px;
+}
+
+.icon-button {
+ width: 34px;
+ min-height: 34px;
+ padding: 0;
+ border: 1px solid var(--border);
+ background: white;
+ color: var(--secondary);
+ font-size: 22px;
+ line-height: 1;
+}
+
+.icon-button:hover {
+ background: #eef6f9;
+ color: var(--secondary-dark);
+}
+
+.clone-details {
+ min-width: 170px;
+}
+
+.clone-details summary {
+ color: var(--text);
+}
+
+.service-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: auto;
+ min-width: 250px;
+}
+
+.service-actions .btn {
+ margin: 0;
+}
+
+.action-menu {
+ position: relative;
+}
+
+.action-menu summary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ min-height: 34px;
+ border-radius: 8px;
+ color: var(--secondary);
+ font-weight: 900;
+ list-style: none;
+ cursor: pointer;
+}
+
+.action-menu summary::-webkit-details-marker {
+ display: none;
+}
+
+.action-menu summary:hover,
+.action-menu[open] summary {
+ background: #eef6f9;
+ text-decoration: none;
+}
+
+.action-menu-panel {
+ position: absolute;
+ top: calc(100% + 6px);
+ right: 0;
+ z-index: 12;
+ width: 210px;
+ padding: 6px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: white;
+ box-shadow: 0 16px 34px rgba(10, 31, 42, 0.18);
+}
+
+.action-menu-panel form {
+ margin: 0;
+}
+
+.menu-action {
+ width: 100%;
+ padding: 10px 12px;
+ background: white;
+ color: var(--text);
+ text-align: left;
+ font-weight: 700;
+}
+
+.menu-action:hover {
+ background: #eef6f9;
+ color: var(--secondary-dark);
+}
+
+.menu-action-danger {
+ color: var(--danger);
+}
+
+.menu-action-danger:hover {
+ background: var(--danger-bg);
+ color: var(--danger-dark);
+}
+
.running-row td {
background: var(--warning-bg);
}
@@ -640,6 +871,33 @@ pre {
width: 100%;
}
+ .service-title {
+ align-items: flex-start;
+ }
+
+ .service-actions {
+ align-items: stretch;
+ flex-direction: column;
+ min-width: 180px;
+ }
+
+ .service-actions .btn,
+ .service-actions .action-menu,
+ .action-menu summary {
+ width: 100%;
+ }
+
+ .action-menu-panel {
+ position: static;
+ width: 100%;
+ margin-top: 6px;
+ }
+
+ .modal-actions {
+ align-items: stretch;
+ flex-direction: column-reverse;
+ }
+
.pagination {
align-items: flex-start;
flex-direction: column;