From 27b2759b00e89d9c9112f71ac65cb7e2a2658f19 Mon Sep 17 00:00:00 2001 From: master <> Date: Fri, 27 Mar 2026 13:20:25 +0200 Subject: [PATCH] Promote button logic, delete confirmation, tooltips, and AGENTS.md conventions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U2 — Promote button three-state model: - showPromote: visible only when a next promotion target exists - canPromote: enabled only when deployed + no blocking gates - promoteDisabledReason: tooltip explaining why promotion is disabled - Added .rdp__btn--disabled style (opacity + pointer-events) W1 — Script delete confirmation: - Replace window.confirm() with app-confirm-dialog variant="danger" - Names the script being deleted in the confirmation message W2 — Script description tooltip: - Add [title] binding to truncated description text in scripts table V1 — Remove duplicate "Profile" h2 in User Preferences tab panel X1 — Breadcrumb root "Ops" → "Operations" to match sidebar group label AGENTS.md — Three new mandatory conventions: - Destructive Action Convention: all deletes/revokes must use app-confirm-dialog - Truncated Text Convention: all text-overflow:ellipsis elements must have [title] - Promote Button Convention: three-state (hidden/disabled/enabled) model Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Web/StellaOps.Web/AGENTS.md | 43 +++++++++++++++++++ src/Web/StellaOps.Web/src/app/app.routes.ts | 4 +- .../releases/release-detail-page.component.ts | 11 ++++- .../releases/state/release-detail.store.ts | 24 ++++++++++- .../scripts/scripts-list.component.ts | 25 +++++++++-- .../user-preferences-page.component.ts | 1 - 6 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/Web/StellaOps.Web/AGENTS.md b/src/Web/StellaOps.Web/AGENTS.md index 4bb782162..7adfec816 100644 --- a/src/Web/StellaOps.Web/AGENTS.md +++ b/src/Web/StellaOps.Web/AGENTS.md @@ -87,6 +87,49 @@ Design and build the StellaOps web user experience that surfaces backend capabil - 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change. - 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context. +## Destructive Action Convention (MANDATORY) + +All destructive actions (delete, revoke, purge, reset) **must** use `` — never `window.confirm()` or unguarded inline handlers. + +**Rules:** +- Every destructive button must open a styled `` with `variant="danger"` before executing +- The confirm dialog message must name the resource being destroyed (e.g., "Delete script 'Pre-deploy Health Check'?") +- Include "This cannot be undone." or equivalent irreversibility warning when applicable +- Never perform destructive API calls directly from a `(click)` handler without confirmation + +**Pattern:** +```html + +``` + +## Truncated Text Convention (MANDATORY) + +All text that may be truncated by CSS (`text-overflow: ellipsis`, table cell overflow, or max-width constraints) **must** have a `[title]` attribute binding to the full untruncated text. + +**Rules:** +- Table cells with descriptions, names, digests, or IDs that truncate must include `[title]="fullValue"` +- Metric card labels already have `[title]="label"` (built into `stella-metric-card`) — do not add a second one +- For dynamically computed truncation, prefer `[title]` over custom tooltip directives for simplicity and offline compatibility +- Ensure tooltips are also applied to `` and `` elements inside table cells when they use `text-overflow: ellipsis` + +## Promote Button Convention (MANDATORY) + +The Promote button on release detail pages must follow a three-state model: + +1. **Hidden** — no further promotion path exists (single-environment release, or already at the final environment in the promotion graph) +2. **Disabled** — promotion path exists but preconditions are not met: + - Release is not yet deployed on the current environment (status = `draft`) + - Blocking gates are unresolved + - Tooltip must explain why promotion is disabled +3. **Enabled** — release is deployed, gates are clear, and a next environment exists + +Use `showPromote` (computed, boolean) for visibility and `canPromote` (computed, boolean) for the enabled/disabled state. +Use `promoteDisabledReason` (computed, string | null) for the disabled tooltip. + ## Metric / KPI Cards Convention (MANDATORY) All metric badges, stat cards, KPI tiles, and summary indicators **must** use ``. diff --git a/src/Web/StellaOps.Web/src/app/app.routes.ts b/src/Web/StellaOps.Web/src/app/app.routes.ts index 05382d6f2..ad066e22e 100644 --- a/src/Web/StellaOps.Web/src/app/app.routes.ts +++ b/src/Web/StellaOps.Web/src/app/app.routes.ts @@ -163,9 +163,9 @@ export const routes: Routes = [ }, { path: 'ops', - title: 'Ops', + title: 'Operations', canMatch: [requireConfigGuard, requireBackendsReachableGuard, requireAuthGuard, requireOpsGuard], - data: { breadcrumb: 'Ops' }, + data: { breadcrumb: 'Operations' }, loadChildren: () => import('./routes/ops.routes').then((m) => m.OPS_ROUTES), }, { diff --git a/src/Web/StellaOps.Web/src/app/features/releases/release-detail-page.component.ts b/src/Web/StellaOps.Web/src/app/features/releases/release-detail-page.component.ts index 59f3e47ef..793f3b205 100644 --- a/src/Web/StellaOps.Web/src/app/features/releases/release-detail-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/releases/release-detail-page.component.ts @@ -95,8 +95,14 @@ const TABS: StellaPageTab[] = [ @if (store.canDeploy()) { } - @if (store.canPromote()) { - Promote + @if (store.showPromote()) { + Promote } @if (store.canRollback()) { @@ -370,6 +376,7 @@ const TABS: StellaPageTab[] = [ .rdp__btn--deploy:hover { opacity: 0.9; } .rdp__btn--promote { background: #00695C; color: #fff; } .rdp__btn--promote:hover { opacity: 0.9; } + .rdp__btn--disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } .rdp__btn--rollback { background: var(--color-status-warning, #C89820); color: #fff; } .rdp__btn--rollback:hover { opacity: 0.9; } diff --git a/src/Web/StellaOps.Web/src/app/features/releases/state/release-detail.store.ts b/src/Web/StellaOps.Web/src/app/features/releases/state/release-detail.store.ts index a35f6a87b..763494933 100644 --- a/src/Web/StellaOps.Web/src/app/features/releases/state/release-detail.store.ts +++ b/src/Web/StellaOps.Web/src/app/features/releases/state/release-detail.store.ts @@ -138,8 +138,30 @@ export class ReleaseDetailStore { return summary?.gates.some(g => g.status === 'BLOCK') ?? false; }); + /** Whether the Promote button should be visible (has a promotion path at all). */ + readonly showPromote = computed(() => { + return this.release() !== null && this.nextPromotionTarget() !== null; + }); + + /** Whether the Promote action is enabled (deployed on current env, no blocking gates). */ readonly canPromote = computed(() => { - return this.release() !== null && !this.hasBlockingGates(); + if (!this.release()) return false; + if (this.hasBlockingGates()) return false; + if (!this.nextPromotionTarget()) return false; + // Must be deployed on current environment before promoting further + const r = this.release()!; + if (r.status === 'draft') return false; + return true; + }); + + /** Reason text when Promote is disabled but visible. */ + readonly promoteDisabledReason = computed(() => { + if (!this.showPromote()) return null; + if (this.canPromote()) return null; + if (this.hasBlockingGates()) return 'Resolve blocking gates before promoting'; + const r = this.release(); + if (r?.status === 'draft') return 'Deploy to the current environment first'; + return 'Not eligible for promotion'; }); readonly currentEnvironments = computed(() => { diff --git a/src/Web/StellaOps.Web/src/app/features/scripts/scripts-list.component.ts b/src/Web/StellaOps.Web/src/app/features/scripts/scripts-list.component.ts index 65341c3ca..8fec040af 100644 --- a/src/Web/StellaOps.Web/src/app/features/scripts/scripts-list.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/scripts/scripts-list.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ViewChild, computed, inject, OnDestroy, OnInit, signal } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; import { take } from 'rxjs'; @@ -8,12 +8,13 @@ import { PageActionService } from '../../core/services/page-action.service'; import { PageActionOutletComponent } from '../../shared/components/page-action-outlet/page-action-outlet.component'; import { StellaFilterChipComponent, FilterChipOption } from '../../shared/components/stella-filter-chip/stella-filter-chip.component'; import { PaginationComponent, PageChangeEvent } from '../../shared/components/pagination/pagination.component'; +import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; import { DateFormatService } from '../../core/i18n/date-format.service'; @Component({ selector: 'app-scripts-list', standalone: true, - imports: [RouterLink, StellaFilterChipComponent, PaginationComponent, PageActionOutletComponent], + imports: [RouterLink, StellaFilterChipComponent, PaginationComponent, PageActionOutletComponent, ConfirmDialogComponent], template: `
@@ -89,7 +90,7 @@ import { DateFormatService } from '../../core/i18n/date-format.service'; {{ s.name }} @if (s.description) { - {{ s.description }} + {{ s.description }} } {{ langLabel(s.language) }} @@ -107,6 +108,12 @@ import { DateFormatService } from '../../core/i18n/date-format.service'; + +
(null); + @ViewChild('deleteConfirm') deleteConfirm!: ConfirmDialogComponent; + confirmDelete(script: Script): void { - if (!confirm(`Delete script "${script.name}"? This cannot be undone.`)) return; + this.pendingDeleteScript.set(script); + this.deleteConfirm.open(); + } + + executeDelete(): void { + const script = this.pendingDeleteScript(); + if (!script) return; this.api.deleteScript(script.id).pipe(take(1)).subscribe({ next: () => { this.scripts.update((list) => list.filter((s) => s.id !== script.id)); + this.pendingDeleteScript.set(null); }, }); } diff --git a/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts b/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts index 543c2a448..21c398169 100644 --- a/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts @@ -66,7 +66,6 @@ type LocaleSaveState = 'idle' | 'saving' | 'saved' | 'syncFailed';
-

Profile

Manage your account identity and contact details