fix+style: deploy modal/banner, back button, table stability
버그픽스 - 배포 확정 활성화: tojson 따옴표가 oninput 속성을 깨뜨리던 것 → data-confirm 비교로 수정 - ghost 버튼 호버 시 글자 하얘짐 → 호버 색 명시(새로고침/페이저/테마토글 공통) UX/디자인 - 배포 모달: 취소·배포확정 한 줄, 저장소명만 mono, 순 변화 +초록/−빨강, 표 chrome 제거 - 배포 성공: 별도 다이얼로그 제거 → 모달 닫힘 + 배지 이동 + 완료 배너(여백·크기 정돈) - 대시보드 백링크: ghost 버튼 + 화살표, 제목과 간격 확보 - 표 액션 셀 고정 박스(172x36) → 동기화/완료/진행 전환 시 행·열 안 흔들림 (완료 'v3', 진행 'n/m·NN%', 실패는 축약+title 로 사유) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,8 +170,9 @@ section {
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
.theme-toggle:hover {
|
.theme-toggle:hover {
|
||||||
--pico-background-color: var(--bg-alt);
|
--pico-background-color: var(--surface);
|
||||||
--pico-border-color: var(--text-dim);
|
--pico-border-color: var(--text-dim);
|
||||||
|
--pico-color: var(--text);
|
||||||
}
|
}
|
||||||
.theme-toggle .icon {
|
.theme-toggle .icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
@@ -230,6 +231,7 @@ button,
|
|||||||
transition:
|
transition:
|
||||||
background-color 0.12s ease,
|
background-color 0.12s ease,
|
||||||
border-color 0.12s ease,
|
border-color 0.12s ease,
|
||||||
|
opacity 0.15s ease,
|
||||||
transform 0.096s ease;
|
transform 0.096s ease;
|
||||||
}
|
}
|
||||||
button:active,
|
button:active,
|
||||||
@@ -259,8 +261,9 @@ button.ghost,
|
|||||||
}
|
}
|
||||||
button.ghost:hover,
|
button.ghost:hover,
|
||||||
[role="button"].ghost:hover {
|
[role="button"].ghost:hover {
|
||||||
--pico-background-color: var(--bg-alt);
|
--pico-background-color: var(--surface);
|
||||||
--pico-border-color: var(--text-dim);
|
--pico-border-color: var(--text-dim);
|
||||||
|
--pico-color: var(--text); /* 호버 시 글자 하얘짐 방지 */
|
||||||
}
|
}
|
||||||
/* 아이콘 + 글자 버튼 */
|
/* 아이콘 + 글자 버튼 */
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
@@ -378,11 +381,14 @@ th.num {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 액션 셀: swap(버튼→진행률) 시 행 높이 안 흔들리게 고정 */
|
/* 액션 셀: 버튼↔진행률↔완료배지로 내용이 바뀌어도 행 높이·열 너비가 안 변하게
|
||||||
|
너비·높이를 고정한 박스로 만든다. 넘치는 내용은 클립. */
|
||||||
.row-action {
|
.row-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 34px;
|
width: 172px;
|
||||||
|
height: 36px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 검증 배지 칩 (dot + 라벨, 이모지 없음) */
|
/* 검증 배지 칩 (dot + 라벨, 이모지 없음) */
|
||||||
@@ -420,6 +426,12 @@ th.num {
|
|||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 뒤로가기(대시보드) 버튼 — ghost, 제목과 간격 확보 */
|
||||||
|
.back-btn {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 버전 검증 상세 */
|
/* 버전 검증 상세 */
|
||||||
.verify-detail {
|
.verify-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -441,15 +453,105 @@ th.num {
|
|||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 배포 완료 배너 (#flash) */
|
||||||
|
.flash-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 4px 0 22px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 200, 150, 0.1);
|
||||||
|
color: var(--success-text);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.flash-banner .flash-dot {
|
||||||
|
flex: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
.flash-banner .flash-sub {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.flash-banner .flash-warn {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--warning-text);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 배포 확인 모달 — 본문/표/라벨 15px 로 통일 */
|
||||||
|
.deploy-modal h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.deploy-note {
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.deploy-preview {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.deploy-preview,
|
||||||
|
.deploy-preview tr,
|
||||||
|
.deploy-preview th,
|
||||||
|
.deploy-preview td {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.deploy-preview th {
|
||||||
|
width: 30%;
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.deploy-preview td {
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.confirm-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
/* 입력란만 작게 (본문보다 4px 작은 14px + 컴팩트 높이) */
|
||||||
|
.confirm-label input {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
--pico-form-element-spacing-vertical: 0.5rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
/* 모달 버튼: 편안한 표준 크기, 한 줄, 너비 auto */
|
||||||
|
.modal-actions button {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 11px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 동기화 진행률 바 */
|
/* 동기화 진행률 바 */
|
||||||
.sync-progress {
|
.sync-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
min-width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
.sync-progress progress {
|
.sync-progress progress {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
.sync-progress small {
|
.sync-progress small {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<dialog open>
|
<dialog open>
|
||||||
<article>
|
<article class="deploy-modal">
|
||||||
<header>
|
<header>
|
||||||
<h3>이 버전을 운영망에 배포</h3>
|
<h3>이 버전을 운영망에 배포</h3>
|
||||||
</header>
|
</header>
|
||||||
@@ -8,34 +8,43 @@
|
|||||||
<p class="status-line status-bad">{{ error }}</p>
|
<p class="status-line status-bad">{{ error }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>운영 서버(yum/dnf)가 실제로 내려받는 버전이 바뀝니다. 신중히 확인하세요.</p>
|
<p class="deploy-note">운영 서버(yum/dnf)가 실제로 내려받는 버전이 바뀝니다. 신중히 확인하세요.</p>
|
||||||
|
|
||||||
{% if preview %}
|
{% if preview %}
|
||||||
<table>
|
<table class="deploy-preview">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>현재 운영</td>
|
<tr>
|
||||||
<td class="num">{% if preview.current_version is not none %}v{{ preview.current_version }} ({{ preview.current_count }}개){% else %}없음{% endif %}</td></tr>
|
<th>현재 운영</th>
|
||||||
<tr><td>배포 대상</td>
|
<td class="num">{% if preview.current_version is not none %}v{{ preview.current_version }} · {{ preview.current_count }}개{% else %}없음{% endif %}</td>
|
||||||
<td class="num">v{{ preview.target_version }} ({{ preview.target_count }}개)</td></tr>
|
</tr>
|
||||||
<tr><td>순 변화</td>
|
<tr>
|
||||||
<td class="num">{{ preview.net_sign }}{{ preview.net }}개</td></tr>
|
<th>배포 대상</th>
|
||||||
|
<td class="num">v{{ preview.target_version }} · {{ preview.target_count }}개</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>순 변화</th>
|
||||||
|
<td class="num">
|
||||||
|
<span class="{{ 'delta-add' if preview.net_sign == '+' else 'delta-del' }}">{{ preview.net_sign }}{{ preview.net }}개</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form hx-post="/repos/{{ uuid }}/deploy" hx-target="#modal" hx-swap="innerHTML">
|
<form hx-post="/repos/{{ uuid }}/deploy" hx-target="#modal" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="version_href" value="{{ version_href }}">
|
<input type="hidden" name="version_href" value="{{ version_href }}">
|
||||||
<label>
|
<label class="confirm-label">
|
||||||
확인을 위해 저장소 이름 <strong>{{ repo_name }}</strong> 을(를) 입력하세요
|
확인을 위해 저장소 이름 <strong class="mono">{{ repo_name }}</strong> 을(를) 입력하세요
|
||||||
<input type="text" name="confirm_name" autocomplete="off" required
|
<input type="text" name="confirm_name" autocomplete="off" required class="mono"
|
||||||
oninput="document.getElementById('deploy-confirm-btn').disabled = (this.value !== {{ repo_name | tojson }})">
|
data-confirm="{{ repo_name }}"
|
||||||
|
oninput="document.getElementById('deploy-confirm-btn').disabled = (this.value !== this.dataset.confirm)">
|
||||||
</label>
|
</label>
|
||||||
{# TODO(auth): 로그인 도입 시 여기서 배포 비밀번호 재확인 입력 추가 #}
|
{# TODO(auth): 로그인 도입 시 배포 비밀번호 재확인 입력 추가 #}
|
||||||
<footer>
|
<div class="modal-actions">
|
||||||
<button type="button" class="secondary"
|
<button type="button" class="ghost"
|
||||||
onclick="document.getElementById('modal').innerHTML=''">취소</button>
|
onclick="document.getElementById('modal').innerHTML=''">취소</button>
|
||||||
<button type="submit" id="deploy-confirm-btn" class="danger" disabled>배포 확정</button>
|
<button type="submit" id="deploy-confirm-btn" class="danger" disabled>배포 확정</button>
|
||||||
</footer>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
{# 배포 성공: 버전 목록을 OOB 로 갱신 + flash 배너 + 모달 자리에 완료 알림 #}
|
{# 배포 성공: (1) 버전 목록 OOB 갱신 → 배지 이동, (2) 완료 배너 OOB,
|
||||||
|
(3) 본문은 비움 → #modal 닫힘. 별도 성공 다이얼로그/닫기 버튼 없음. #}
|
||||||
{% set oob = true %}
|
{% set oob = true %}
|
||||||
{% include "partials/version_list.html" %}
|
{% include "partials/version_list.html" %}
|
||||||
|
|
||||||
<div id="flash" hx-swap-oob="true">
|
<div id="flash" hx-swap-oob="true">
|
||||||
<p class="status-line status-ok">
|
<div class="flash-banner">
|
||||||
배포 완료 · v{{ to_version }}{% if from_version is not none %} (이전 v{{ from_version }}){% endif %}
|
<span class="flash-dot"></span>
|
||||||
{% if not audit_ok %} — ⚠ 감사 로그 기록 실패{% endif %}
|
<span>배포 완료 · <strong class="mono">v{{ to_version }}</strong>{% if from_version is not none %} <span class="flash-sub">(이전 v{{ from_version }})</span>{% endif %}</span>
|
||||||
</p>
|
{% if not audit_ok %}<span class="flash-warn">감사 로그 기록 실패</span>{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dialog open>
|
|
||||||
<article>
|
|
||||||
<header><h3>배포 완료</h3></header>
|
|
||||||
<p>운영 배포 버전이 <strong>v{{ to_version }}</strong> 로 교체되었습니다.</p>
|
|
||||||
<footer>
|
|
||||||
<button onclick="document.getElementById('modal').innerHTML=''">닫기</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
</dialog>
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
{# 진행률 바 조각. running 일 때만 hx-trigger 를 달아 3초 폴링; 완료/실패 시 폴링 중단. #}
|
{# 진행률 바 조각. running 일 때만 hx-trigger 를 달아 3초 폴링; 완료/실패 시 폴링 중단.
|
||||||
|
내용은 .row-action 고정 박스(172x36) 안에 들어오도록 짧게 유지. #}
|
||||||
<div id="prog-{{ uuid }}"
|
<div id="prog-{{ uuid }}"
|
||||||
{% if progress.running %}hx-get="/repos/{{ uuid }}/progress" hx-trigger="every 3s" hx-swap="outerHTML"{% endif %}>
|
{% if progress.running %}hx-get="/repos/{{ uuid }}/progress" hx-trigger="every 3s" hx-swap="outerHTML"{% endif %}>
|
||||||
{% if progress.failed %}
|
{% if progress.failed %}
|
||||||
<span class="status-line status-bad">동기화 실패: {{ progress.error }}</span>
|
<span class="status-line status-bad" title="{{ progress.error }}">동기화 실패</span>
|
||||||
{% elif progress.completed %}
|
{% elif progress.completed %}
|
||||||
<span class="badge badge-pass">동기화 완료{% if progress.new_version %} · v{{ progress.new_version }} 생성됨{% endif %}</span>
|
<span class="badge badge-pass">완료{% if progress.new_version %} · v{{ progress.new_version }}{% endif %}</span>
|
||||||
{% elif progress.idle %}
|
{% elif progress.idle %}
|
||||||
{# 추적 중인 작업 없음 — 빈 조각 #}
|
{# 추적 중인 작업 없음 — 빈 조각 #}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="sync-progress">
|
<div class="sync-progress">
|
||||||
{% if progress.total %}
|
{% if progress.total %}
|
||||||
<progress value="{{ progress.done }}" max="{{ progress.total }}"></progress>
|
<progress value="{{ progress.done }}" max="{{ progress.total }}"></progress>
|
||||||
<small class="num">{{ progress.done }}/{{ progress.total }} 패키지 · {{ progress.percent }}%</small>
|
<small class="num">{{ progress.done }}/{{ progress.total }} · {{ progress.percent }}%</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<progress></progress>
|
<progress></progress>
|
||||||
<small>동기화 준비 중…</small>
|
<small>준비 중…</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p><a href="/">← 대시보드</a></p>
|
<a href="/" role="button" class="ghost btn-sm btn-icon back-btn">
|
||||||
|
<svg class="icon" viewBox="0 0 16 16" width="13" height="13" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" aria-hidden="true"><path d="M10 2.5 4.5 8l5.5 5.5"/></svg>
|
||||||
|
대시보드
|
||||||
|
</a>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>{{ repo_name }} · 버전</h2>
|
<h2>{{ repo_name }} · 버전</h2>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ def test_progress_completed_stops_polling_and_clears():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
resp = client.get("/repos/abc/progress")
|
resp = client.get("/repos/abc/progress")
|
||||||
assert "동기화 완료" in resp.text
|
assert "완료" in resp.text
|
||||||
assert "v7" in resp.text
|
assert "v7" in resp.text
|
||||||
assert "hx-trigger" not in resp.text # 폴링 중단
|
assert "hx-trigger" not in resp.text # 폴링 중단
|
||||||
assert task_store.get_task("abc") is None # 정리됨
|
assert task_store.get_task("abc") is None # 정리됨
|
||||||
|
|||||||
Reference in New Issue
Block a user