+
+
+ @if (error(); as err) {
+
{{ err }}
+ }
+
+
+ @if (editing()) {
+
+ }
+
+
+ @if (loading()) {
+
Loading policies...
+ } @else if (policies().length === 0 && !editing()) {
+
+ No scan policies found. Create one to get started.
+
+ } @else {
+
+
+
+
+ | Name |
+ Description |
+ Status |
+ Rules |
+ Updated |
+ Actions |
+
+
+
+ @for (policy of policies(); track policy.id) {
+
+ | {{ policy.name }} |
+ {{ policy.description || '--' }} |
+
+
+ {{ policy.enabled ? 'Enabled' : 'Disabled' }}
+
+ |
+ {{ policy.rules.length }} |
+ {{ formatDate(policy.updatedAt) }} |
+
+
+
+ |
+
+ }
+
+
+
+ }
+
+ `,
+ styles: [`
+ .scan-policy-page {
+ display: grid;
+ gap: 1rem;
+ max-width: 960px;
+ margin: 0 auto;
+ }
+
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .page-header h1 { margin: 0; }
+
+ .page-header p {
+ margin: 0.2rem 0 0;
+ color: var(--color-text-secondary);
+ font-size: 0.8rem;
+ }
+
+ /* Cards */
+ .form-card,
+ .table-card,
+ .empty-state {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-surface-primary);
+ padding: 0.9rem;
+ }
+
+ .form-card {
+ display: grid;
+ gap: 0.75rem;
+ }
+
+ .form-card h2 { margin: 0; font-size: 1rem; }
+
+ /* Form fields */
+ .form-field {
+ display: grid;
+ gap: 0.25rem;
+ font-size: 0.78rem;
+ color: var(--color-text-secondary);
+ }
+
+ input[type="text"],
+ textarea,
+ select {
+ width: 100%;
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-sm);
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ padding: 0.4rem 0.5rem;
+ font-size: 0.8rem;
+ font-family: inherit;
+ }
+
+ select { width: auto; min-width: 120px; }
+
+ .checkbox-row {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.45rem;
+ color: var(--color-text-primary);
+ font-size: 0.8rem;
+ }
+
+ .checkbox-row input[type="checkbox"] { width: auto; }
+
+ /* Rules */
+ .rules-section {
+ display: grid;
+ gap: 0.5rem;
+ }
+
+ .rules-section h3 {
+ margin: 0;
+ font-size: 0.85rem;
+ }
+
+ .rule-row {
+ display: flex;
+ gap: 0.35rem;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ /* Buttons */
+ .btn-primary,
+ .btn-ghost {
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-sm);
+ padding: 0.35rem 0.6rem;
+ font-size: 0.78rem;
+ cursor: pointer;
+ background: var(--color-surface-primary);
+ color: var(--color-text-primary);
+ font-family: inherit;
+ }
+
+ .btn-primary {
+ border-color: var(--color-brand-primary);
+ background: var(--color-brand-primary);
+ color: var(--color-text-heading);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ }
+
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
+
+ .btn-ghost:disabled { opacity: 0.4; cursor: not-allowed; }
+
+ .btn-icon { padding: 0.25rem; line-height: 0; }
+
+ .btn-danger { color: var(--color-status-error-text); }
+
+ .form-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ justify-content: flex-end;
+ }
+
+ /* Table */
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.8rem;
+ }
+
+ th {
+ text-align: left;
+ padding: 0.5rem 0.6rem;
+ border-bottom: 1px solid var(--color-border-primary);
+ color: var(--color-text-secondary);
+ font-weight: 600;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ td {
+ padding: 0.5rem 0.6rem;
+ border-bottom: 1px solid var(--color-border-primary);
+ color: var(--color-text-primary);
+ }
+
+ .cell-name { font-weight: 500; }
+ .cell-desc { color: var(--color-text-secondary); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .cell-date { font-size: 0.76rem; color: var(--color-text-secondary); white-space: nowrap; }
+ .cell-actions { display: flex; gap: 0.35rem; }
+
+ /* Status badges */
+ .status-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.15rem 0.45rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ .status-badge[data-status="enabled"] {
+ background: color-mix(in srgb, var(--color-status-success-text) 15%, transparent);
+ color: var(--color-status-success-text);
+ }
+
+ .status-badge[data-status="disabled"] {
+ background: color-mix(in srgb, var(--color-text-secondary) 15%, transparent);
+ color: var(--color-text-secondary);
+ }
+
+ /* Spinner */
+ .spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+ }
+
+ @keyframes spin { to { transform: rotate(360deg); } }
+
+ .loading-text { font-size: 0.8rem; color: var(--color-text-secondary); }
+ .empty-state { text-align: center; padding: 2rem; }
+ .empty-state p { color: var(--color-text-secondary); font-size: 0.85rem; margin: 0; }
+ .error-message { margin: 0; font-size: 0.78rem; color: var(--color-status-error-text); }
+ `],
+})
+export class ScanPolicyComponent {
+ private readonly http = inject(HttpClient);
+ private readonly destroyRef = inject(DestroyRef);
+
+ private static readonly API_BASE = '/api/v1/scan-policies';
+
+ // List state
+ readonly policies = signal