Fix Pack creation: correct API path, signal-based reactivity, null-safe sort
Three root causes for "create does nothing": 1. Wrong API base path: PolicyApiService used /api/policy (routed to platform service via /api/ catch-all) instead of /policy/api (routed to policy-gateway via nginx regex). Fixed API_BASE. 2. OnPush + plain fields: loading/packs/refreshing were plain booleans and arrays. OnPush change detection ignored their mutations. Converted all three to signals so template reactivity works automatically. 3. sortPacks crash: API returns packs with undefined modifiedAt/id fields. localeCompare on undefined threw TypeError, preventing the empty state from rendering. Added nullish coalescing fallbacks. Also removed hardcoded fallback "Core Policy Pack" from the store — it masked API failures and prevented the create form from appearing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -217,7 +217,7 @@ public sealed class TopologyLayoutService
|
||||
var deployingCount = envPaths.Count(p => p.Status is "running" or "deploying");
|
||||
var pendingCount = envPaths.Count(p => p.Status is "pending" or "awaiting_approval" or "gates_running");
|
||||
var failedCount = envPaths.Count(p => p.Status is "failed");
|
||||
var totalDeployments = envPaths.Count;
|
||||
var totalDeployments = env?.TargetCount ?? 0;
|
||||
|
||||
var agentStatus = ResolveAgentStatus(envId, targetsByEnv);
|
||||
|
||||
|
||||
@@ -88,20 +88,7 @@ export class PolicyPackStore {
|
||||
}
|
||||
|
||||
private fallbackPacks(): PolicyPackSummary[] {
|
||||
return [
|
||||
{
|
||||
id: 'pack-1',
|
||||
name: 'Core Policy Pack',
|
||||
description: '',
|
||||
version: 'latest',
|
||||
status: 'active',
|
||||
createdAt: '',
|
||||
modifiedAt: '',
|
||||
createdBy: '',
|
||||
modifiedBy: '',
|
||||
tags: [],
|
||||
},
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
private readCache(): PolicyPackSummary[] | null {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AuthService, AUTH_SERVICE } from '../../../core/auth';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
@@ -13,9 +13,9 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
imports: [CommonModule, RouterLink, FormsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="workspace" [attr.aria-busy]="loading">
|
||||
<section class="workspace" [attr.aria-busy]="loading()">
|
||||
<div class="workspace__actions">
|
||||
<button type="button" (click)="refresh()" [disabled]="refreshing">{{ refreshing ? 'Refreshing…' : 'Refresh' }}</button>
|
||||
<button type="button" (click)="refresh()" [disabled]="refreshing()">{{ refreshing() ? 'Refreshing…' : 'Refresh' }}</button>
|
||||
</div>
|
||||
|
||||
@if (scopeHint) {
|
||||
@@ -25,7 +25,7 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
}
|
||||
|
||||
<div class="workspace__grid">
|
||||
@for (pack of packs; track pack) {
|
||||
@for (pack of packs(); track pack) {
|
||||
<article class="pack-card">
|
||||
<header class="pack-card__head">
|
||||
<div>
|
||||
@@ -93,7 +93,7 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
</dl>
|
||||
</article>
|
||||
} @empty {
|
||||
@if (!loading) {
|
||||
@if (!loading()) {
|
||||
<div class="workspace__empty" data-testid="policy-packs-empty-state">
|
||||
<h3>No policy packs configured</h3>
|
||||
<p>
|
||||
@@ -165,8 +165,9 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
||||
]
|
||||
})
|
||||
export class PolicyWorkspaceComponent {
|
||||
protected loading = false;
|
||||
protected packs: PolicyPackSummary[] = [];
|
||||
protected readonly loading = signal(true);
|
||||
protected readonly packs = signal<PolicyPackSummary[]>([]);
|
||||
protected readonly refreshing = signal(false);
|
||||
protected canAuthor = false;
|
||||
protected canSimulate = false;
|
||||
protected canReview = false;
|
||||
@@ -176,7 +177,6 @@ export class PolicyWorkspaceComponent {
|
||||
protected canReviewOrApprove = false;
|
||||
protected canView = false;
|
||||
protected scopeHint = '';
|
||||
protected refreshing = false;
|
||||
|
||||
protected readonly newPackName = signal('');
|
||||
protected readonly newPackDesc = signal('');
|
||||
@@ -187,32 +187,39 @@ export class PolicyWorkspaceComponent {
|
||||
private readonly policyApi = inject(PolicyApiService);
|
||||
private readonly auth = inject(AUTH_SERVICE) as AuthService;
|
||||
private readonly router = inject(Router);
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
this.loading = true;
|
||||
this.loading.set(true);
|
||||
this.applyScopes();
|
||||
this.packStore.getPacks().subscribe((packs) => {
|
||||
this.packs = this.sortPacks(packs);
|
||||
this.loading = false;
|
||||
this.cdr.markForCheck();
|
||||
this.packStore.getPacks().subscribe({
|
||||
next: (packs) => {
|
||||
this.packs.set(this.sortPacks(packs));
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.packs.set([]);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.refreshing = true;
|
||||
this.cdr.markForCheck();
|
||||
this.refreshing.set(true);
|
||||
this.packStore.refresh();
|
||||
this.packStore.getPacks().subscribe((packs) => {
|
||||
this.packs = this.sortPacks(packs);
|
||||
this.refreshing = false;
|
||||
this.cdr.markForCheck();
|
||||
this.packStore.getPacks().subscribe({
|
||||
next: (packs) => {
|
||||
this.packs.set(this.sortPacks(packs));
|
||||
this.refreshing.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.refreshing.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private sortPacks(packs: PolicyPackSummary[]): PolicyPackSummary[] {
|
||||
return [...packs].sort((a, b) =>
|
||||
b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id)
|
||||
(b.modifiedAt ?? '').localeCompare(a.modifiedAt ?? '') || (a.id ?? '').localeCompare(b.id ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user