Adds website theme serialization system

This commit is contained in:
Your Name
2026-07-06 06:52:18 +00:00
parent b932d65658
commit 8cd54459ec
29 changed files with 418 additions and 4 deletions
+28 -1
View File
@@ -22,7 +22,7 @@
<button class="tab-btn" [class.active]="activeTab() === 'underwriters'"
(click)="activeTab.set('underwriters')">Underwriters</button>
<button class="tab-btn" [class.active]="activeTab() === 'mobile-builds'"
(click)="activeTab.set('mobile-builds')">Mobile Builds</button>
(click)="activeTab.set('mobile-builds'); loadThemeJsonStatus()">Mobile Builds</button>
<button class="tab-btn" [class.active]="activeTab() === 'stats'" (click)="activeTab.set('stats')">Stats</button>
</div>
@@ -437,6 +437,33 @@
}
</section>
<section class="build-card">
<h3>Theme Export</h3>
@if (themeJsonStatus$ | async; as status) {
@if (status.exists) {
<div class="theme-status-ok">
<div><strong>theme.json</strong> — ready</div>
<div>Path: {{ status.path }}</div>
<div>Generated: {{ status.generated_at }}</div>
<div>Size: {{ status.size }} bytes</div>
</div>
} @else {
<div class="theme-status-missing">
<p><strong>theme.json</strong> not found at {{ status.path }}</p>
<p>Regenerate to create it from the current station config.</p>
</div>
}
<button class="btn btn-outline" (click)="regenerateThemeJson()"
[disabled]="themeJsonLoading$ | async">
{{ (themeJsonLoading$ | async) ? 'Regenerating...' : 'Regenerate' }}
</button>
} @else {
@if (themeJsonLoading$ | async) {
<p class="empty-row">Loading...</p>
}
}
</section>
<section class="build-card">
<h3>Create Build Request</h3>
+54
View File
@@ -539,6 +539,60 @@
font-size: 0.8rem;
}
// ── Theme export status ──────────────────────────────────
.theme-status-ok,
.theme-status-missing {
padding: $spacing-sm $spacing-md;
border-radius: $radius-sm;
margin-bottom: $spacing-sm;
font-size: 0.88rem;
color: $neutral-dark;
}
.theme-status-ok {
background: rgba($success-green, 0.08);
border: 1px solid rgba($success-green, 0.25);
display: grid;
gap: 2px;
}
.theme-status-missing p {
margin: 0 0 $spacing-xs;
&:last-child {
margin-bottom: 0;
}
}
.theme-status-missing {
background: rgba($accent-orange, 0.08);
border: 1px solid rgba($accent-orange, 0.25);
}
.btn-outline {
background: transparent;
border: 2px solid $primary-blue;
color: $primary-blue;
border-radius: $radius-md;
padding: $spacing-xs $spacing-md;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: background $transition-fast, color $transition-fast;
margin-top: $spacing-sm;
&:hover:not(:disabled) {
background: $primary-blue;
color: $neutral-white;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
@include responsive(md) {
.mobile-build-grid {
grid-template-columns: 1fr;
+35
View File
@@ -13,6 +13,7 @@ import { Underwriter } from '../interfaces/underwriter';
import {
MobileBuildDefaults,
MobileBuildRequest,
ThemeStatus,
} from '../interfaces/mobile-build';
import { ShowService } from '../services/show.service';
import { EventService } from '../services/event.service';
@@ -126,6 +127,10 @@ export class AdminComponent implements OnInit {
mobileBuildPreflightIssues: string[] = [];
mobileBuildBusy = false;
// ── Theme JSON status (mobile build tab) ────────────────────
readonly themeJsonStatus$ = new BehaviorSubject<ThemeStatus | null>(null);
readonly themeJsonLoading$ = new BehaviorSubject<boolean>(false);
buildAndroid = true;
buildIos = true;
androidApplicationId = '';
@@ -412,6 +417,36 @@ export class AdminComponent implements OnInit {
this.onMobileBuildSelected(this.selectedMobileBuildId);
}
// ── Theme JSON status ──────────────────────────────────────
async loadThemeJsonStatus(): Promise<void> {
try {
this.themeJsonLoading$.next(true);
this.themeJsonStatus$.next(await firstValueFrom(
this.mobileBuildService.getThemeStatus(),
));
} catch {
this.themeJsonStatus$.next(null);
} finally {
this.themeJsonLoading$.next(false);
}
}
async regenerateThemeJson(): Promise<void> {
try {
this.themeJsonLoading$.next(true);
await firstValueFrom(this.mobileBuildService.regenerateTheme());
await this.loadThemeJsonStatus();
} catch (err) {
this.mobileBuildErrorMessage = this.getErrorMessage(
err,
'Failed to regenerate theme.json.',
);
} finally {
this.themeJsonLoading$.next(false);
}
}
getArtifactDownloadHref(path: string): string {
return `${getAppConfig().apiBaseUrl}${path}`;
}
+7
View File
@@ -67,3 +67,10 @@ export interface MobileBuildPreflightResult {
passed: boolean;
issues: string[];
}
export interface ThemeStatus {
exists: boolean;
path: string;
generated_at: string | null;
size: number | null;
}
+14
View File
@@ -9,6 +9,7 @@ import {
MobileBuildPreflightResult,
MobileBuildRequest,
MobileBuildUploadedFile,
ThemeStatus,
} from '../interfaces/mobile-build';
@Injectable({
@@ -66,4 +67,17 @@ export class MobileBuildService {
{},
);
}
getThemeStatus(): Observable<ThemeStatus> {
return this.http.get<ThemeStatus>(
`${getAppConfig().apiBaseUrl}/api/theme/status`,
);
}
regenerateTheme(): Observable<unknown> {
return this.http.post(
`${getAppConfig().apiBaseUrl}/api/theme/regenerate`,
{},
);
}
}