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:
master
2026-03-29 00:24:21 +02:00
parent d8e6a2a53d
commit 39e1bf0bd4
3 changed files with 30 additions and 36 deletions

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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 ?? '')
);
}