Fix approval API endpoints, pending lane animation, and add No Mockups rule

Approval API fix:
- Frontend was calling /api/v1/approvals/{id}/decision (wrong URL)
- Backend registers at /api/v1/release-orchestrator/approvals/{id}/approve
- Updated queueBaseUrl and detailBaseUrl to match actual backend routes
- Changed approve/reject to POST /{id}/approve and /{id}/reject (not /decision)

Pending actions animation:
- Add CSS keyframe animations for pending-lane enter (fade-in) and exit (collapse)
- onTabChange('approvals') now triggers exit animation (250ms) before switching tab
- Pending lane smoothly collapses when user clicks "View all"

AGENTS.md — No Mockups Convention:
- All UI must connect to real backend endpoints
- Never create mock/stub implementations unless explicitly requested
- Error states must be surfaced to user, never silently swallowed
- If backend endpoint doesn't exist, mark task BLOCKED

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-27 18:11:06 +02:00
parent 149bb9123b
commit 868f94236b
3 changed files with 33 additions and 10 deletions

View File

@@ -87,6 +87,17 @@ 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.
## No Mockups Convention (MANDATORY)
All UI components **must** connect to real backend API endpoints. Never use `window.confirm()`, mock data services, or stub implementations unless explicitly requested by the product owner.
**Rules:**
- Every button, form submission, and action handler must call a real API endpoint
- If the backend endpoint doesn't exist yet, mark the task `BLOCKED` — do not create a mock
- The in-memory mock clients (e.g., `InMemoryApprovalClient`) exist ONLY for `ng serve` without backends, never as production implementations
- Error states from API failures must be surfaced to the user (never silently swallow errors)
- If an API returns 404/500, show the error in a banner or toast — don't pretend the action succeeded
## Destructive Action Convention (MANDATORY)
All destructive actions (delete, revoke, purge, reset) **must** use `<app-confirm-dialog>` — never `window.confirm()` or unguarded inline handlers.

View File

@@ -32,8 +32,8 @@ export interface ApprovalApi {
@Injectable()
export class ApprovalHttpClient implements ApprovalApi {
private readonly http = inject(HttpClient);
private readonly queueBaseUrl = '/api/v2/releases/approvals';
private readonly detailBaseUrl = '/api/v1/approvals';
private readonly queueBaseUrl = '/api/v1/release-orchestrator/approvals';
private readonly detailBaseUrl = '/api/v1/release-orchestrator/approvals';
listApprovals(filter?: ApprovalFilter): Observable<ApprovalRequest[]> {
const params: Record<string, string> = {};
@@ -78,20 +78,16 @@ export class ApprovalHttpClient implements ApprovalApi {
}
approve(id: string, comment: string): Observable<ApprovalDetail> {
return this.http.post<any>(`${this.detailBaseUrl}/${id}/decision`, {
action: 'approve',
return this.http.post<any>(`${this.detailBaseUrl}/${id}/approve`, {
comment,
actor: 'ui-operator',
}).pipe(
map(row => this.mapV2ApprovalDetail(row))
);
}
reject(id: string, comment: string): Observable<ApprovalDetail> {
return this.http.post<any>(`${this.detailBaseUrl}/${id}/decision`, {
action: 'reject',
return this.http.post<any>(`${this.detailBaseUrl}/${id}/reject`, {
comment,
actor: 'ui-operator',
}).pipe(
map(row => this.mapV2ApprovalDetail(row))
);

View File

@@ -72,7 +72,7 @@ function deriveOutcomeIcon(status: string): string {
<!-- Pending approvals inline lane (dashboard-style action cards) -->
@if (pendingApprovals().length > 0 && viewMode() !== 'approvals') {
<div class="pending-lane">
<div class="pending-lane" [class.pending-lane--exiting]="pendingLaneExiting()">
<div class="pending-lane__header">
<h2 class="pending-lane__title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
@@ -390,7 +390,10 @@ function deriveOutcomeIcon(status: string): string {
.apc__status-row{display:flex;gap:.35rem;padding:0 .6rem .45rem;flex-wrap:wrap}
.pending-lane{margin-bottom:1rem;padding:1rem;border:1px solid var(--color-status-warning-border);border-radius:var(--radius-lg);background:var(--color-status-warning-bg)}
.pending-lane{margin-bottom:1rem;padding:1rem;border:1px solid var(--color-status-warning-border);border-radius:var(--radius-lg);background:var(--color-status-warning-bg);animation:lane-enter 300ms ease-out both;overflow:hidden}
.pending-lane--exiting{animation:lane-exit 250ms ease-in forwards}
@keyframes lane-enter{from{opacity:0;max-height:0;padding:0;margin:0;border-width:0}to{opacity:1;max-height:500px}}
@keyframes lane-exit{from{opacity:1;max-height:500px}to{opacity:0;max-height:0;padding:0;margin:0;border-width:0}}
.pending-lane__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}
.pending-lane__title{display:flex;align-items:center;gap:.5rem;margin:0;font-size:.9rem;font-weight:600;color:var(--color-status-warning-text)}
.pending-lane__link{display:inline-flex;align-items:center;gap:.25rem;background:none;border:none;font-size:.8rem;font-weight:500;color:var(--color-status-warning-text);cursor:pointer;text-decoration:underline;text-underline-offset:2px}
@@ -729,7 +732,20 @@ export class ReleasesActivityComponent implements OnInit, OnDestroy {
return parsed.toLocaleString(this.dateFmt.locale(), { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
readonly pendingLaneExiting = signal(false);
onTabChange(tab: string): void {
// Animate pending lane out before switching to approvals
if (tab === 'approvals' && this.pendingApprovals().length > 0 && this.viewMode() !== 'approvals') {
this.pendingLaneExiting.set(true);
setTimeout(() => {
this.pendingLaneExiting.set(false);
this.viewMode.set('approvals');
this.loadApprovals();
this.applyFilters();
}, 250);
return;
}
this.viewMode.set(tab as 'timeline' | 'approvals');
if (tab === 'approvals') this.loadApprovals();
this.applyFilters();