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:
@@ -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.
|
- 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.
|
- 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)
|
## Destructive Action Convention (MANDATORY)
|
||||||
|
|
||||||
All destructive actions (delete, revoke, purge, reset) **must** use `<app-confirm-dialog>` — never `window.confirm()` or unguarded inline handlers.
|
All destructive actions (delete, revoke, purge, reset) **must** use `<app-confirm-dialog>` — never `window.confirm()` or unguarded inline handlers.
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export interface ApprovalApi {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApprovalHttpClient implements ApprovalApi {
|
export class ApprovalHttpClient implements ApprovalApi {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly queueBaseUrl = '/api/v2/releases/approvals';
|
private readonly queueBaseUrl = '/api/v1/release-orchestrator/approvals';
|
||||||
private readonly detailBaseUrl = '/api/v1/approvals';
|
private readonly detailBaseUrl = '/api/v1/release-orchestrator/approvals';
|
||||||
|
|
||||||
listApprovals(filter?: ApprovalFilter): Observable<ApprovalRequest[]> {
|
listApprovals(filter?: ApprovalFilter): Observable<ApprovalRequest[]> {
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
@@ -78,20 +78,16 @@ export class ApprovalHttpClient implements ApprovalApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
approve(id: string, comment: string): Observable<ApprovalDetail> {
|
approve(id: string, comment: string): Observable<ApprovalDetail> {
|
||||||
return this.http.post<any>(`${this.detailBaseUrl}/${id}/decision`, {
|
return this.http.post<any>(`${this.detailBaseUrl}/${id}/approve`, {
|
||||||
action: 'approve',
|
|
||||||
comment,
|
comment,
|
||||||
actor: 'ui-operator',
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
map(row => this.mapV2ApprovalDetail(row))
|
map(row => this.mapV2ApprovalDetail(row))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(id: string, comment: string): Observable<ApprovalDetail> {
|
reject(id: string, comment: string): Observable<ApprovalDetail> {
|
||||||
return this.http.post<any>(`${this.detailBaseUrl}/${id}/decision`, {
|
return this.http.post<any>(`${this.detailBaseUrl}/${id}/reject`, {
|
||||||
action: 'reject',
|
|
||||||
comment,
|
comment,
|
||||||
actor: 'ui-operator',
|
|
||||||
}).pipe(
|
}).pipe(
|
||||||
map(row => this.mapV2ApprovalDetail(row))
|
map(row => this.mapV2ApprovalDetail(row))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function deriveOutcomeIcon(status: string): string {
|
|||||||
|
|
||||||
<!-- Pending approvals inline lane (dashboard-style action cards) -->
|
<!-- Pending approvals inline lane (dashboard-style action cards) -->
|
||||||
@if (pendingApprovals().length > 0 && viewMode() !== 'approvals') {
|
@if (pendingApprovals().length > 0 && viewMode() !== 'approvals') {
|
||||||
<div class="pending-lane">
|
<div class="pending-lane" [class.pending-lane--exiting]="pendingLaneExiting()">
|
||||||
<div class="pending-lane__header">
|
<div class="pending-lane__header">
|
||||||
<h2 class="pending-lane__title">
|
<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">
|
<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}
|
.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__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__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}
|
.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' });
|
return parsed.toLocaleString(this.dateFmt.locale(), { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly pendingLaneExiting = signal(false);
|
||||||
|
|
||||||
onTabChange(tab: string): void {
|
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');
|
this.viewMode.set(tab as 'timeline' | 'approvals');
|
||||||
if (tab === 'approvals') this.loadApprovals();
|
if (tab === 'approvals') this.loadApprovals();
|
||||||
this.applyFilters();
|
this.applyFilters();
|
||||||
|
|||||||
Reference in New Issue
Block a user