sets default admin creds in docker compose. first pass remote builder feature
This commit is contained in:
@@ -47,6 +47,11 @@
|
||||
[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>
|
||||
<button
|
||||
class="tab-btn"
|
||||
[class.active]="activeTab() === 'stats'"
|
||||
@@ -448,6 +453,196 @@
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Mobile Builds Tab -->
|
||||
@if (activeTab() === 'mobile-builds') {
|
||||
<div class="tab-content mobile-builds-panel">
|
||||
<div class="tab-toolbar">
|
||||
<h2>Mobile Build Orchestration</h2>
|
||||
<button class="btn btn-add" (click)="reloadMobileBuilds()">Refresh Requests</button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-build-grid">
|
||||
<section class="build-card">
|
||||
<h3>Derived Defaults</h3>
|
||||
@if (mobileBuildDefaults; as defaults) {
|
||||
<div class="derived-list">
|
||||
<div><strong>Tenant:</strong> {{ defaults.tenant_key }}</div>
|
||||
<div><strong>App Name:</strong> {{ defaults.app_name }}</div>
|
||||
<div><strong>Support Email:</strong> {{ defaults.support_email }}</div>
|
||||
<div><strong>Website:</strong> {{ defaults.website }}</div>
|
||||
<div><strong>Stream URL:</strong> {{ defaults.stream_url || '(not set)' }}</div>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="empty-row">Defaults unavailable. Check station config.</p>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="build-card">
|
||||
<h3>Create Build Request</h3>
|
||||
|
||||
<div class="form-row platform-row">
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="buildAndroid">
|
||||
Android
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" [(ngModel)]="buildIos">
|
||||
iOS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Android Application ID
|
||||
<input type="text" [(ngModel)]="androidApplicationId" placeholder="com.example.radio">
|
||||
</label>
|
||||
<label>
|
||||
iOS Bundle ID
|
||||
<input type="text" [(ngModel)]="iosBundleId" placeholder="com.example.radio">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Version Name
|
||||
<input type="text" [(ngModel)]="buildVersionName" placeholder="1.0.0">
|
||||
</label>
|
||||
<label>
|
||||
Build Number
|
||||
<input type="text" [(ngModel)]="buildNumber" placeholder="1">
|
||||
</label>
|
||||
<label>
|
||||
Release Profile
|
||||
<input type="text" [(ngModel)]="buildReleaseProfile" placeholder="release">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>Required Files</h4>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
Android Keystore (.jks/.keystore)
|
||||
<input type="file" (change)="onFileChosen($event, 'android-keystore')">
|
||||
</label>
|
||||
<label>
|
||||
Android Descriptor (JSON)
|
||||
<input type="file" accept=".json,application/json" (change)="onFileChosen($event, 'android-descriptor')">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>
|
||||
iOS Certificate (.p12)
|
||||
<input type="file" accept=".p12" (change)="onFileChosen($event, 'ios-certificate')">
|
||||
</label>
|
||||
<label>
|
||||
iOS Provisioning Profile (.mobileprovision)
|
||||
<input type="file" accept=".mobileprovision" (change)="onFileChosen($event, 'ios-profile')">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (mobileBuildStatusMessage) {
|
||||
<p class="status success">{{ mobileBuildStatusMessage }}</p>
|
||||
}
|
||||
@if (mobileBuildErrorMessage) {
|
||||
<p class="status error">{{ mobileBuildErrorMessage }}</p>
|
||||
}
|
||||
|
||||
<button
|
||||
class="btn btn-add"
|
||||
[disabled]="mobileBuildBusy"
|
||||
(click)="createAndUploadMobileBuildRequest()"
|
||||
>
|
||||
{{ mobileBuildBusy ? 'Working…' : 'Create Draft + Upload Files' }}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="build-card">
|
||||
<h3>Build Requests</h3>
|
||||
@if (mobileBuilds$ | async; as builds) {
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Platforms</th>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>Preflight</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (build of builds; track build.id) {
|
||||
<tr [class.selected-build]="selectedMobileBuildId === build.id">
|
||||
<td>#{{ build.id }}</td>
|
||||
<td>
|
||||
{{ build.platform_android ? 'Android' : '' }}
|
||||
{{ build.platform_android && build.platform_ios ? ' + ' : '' }}
|
||||
{{ build.platform_ios ? 'iOS' : '' }}
|
||||
</td>
|
||||
<td>{{ build.version_name }} ({{ build.build_number }})</td>
|
||||
<td>{{ build.status }}</td>
|
||||
<td>{{ build.preflight_passed ? 'Passed' : 'Pending/Failed' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn-icon btn-edit" (click)="onMobileBuildSelected(build.id)">Select</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr><td colspan="6" class="empty-row">No build requests yet.</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (selectedMobileBuild; as build) {
|
||||
<section class="build-card">
|
||||
<div class="tab-toolbar compact">
|
||||
<h3>Selected Request #{{ build.id }}</h3>
|
||||
<div class="action-group">
|
||||
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy" (click)="runPreflightForSelectedBuild()">Run Preflight</button>
|
||||
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy || !build.preflight_passed" (click)="triggerSelectedBuild()">Trigger Build</button>
|
||||
<button class="btn-icon btn-edit" [disabled]="mobileBuildBusy" (click)="refreshSelectedBuild()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (mobileBuildPreflightIssues.length > 0) {
|
||||
<div class="status error">
|
||||
<strong>Preflight issues:</strong>
|
||||
<ul>
|
||||
@for (issue of mobileBuildPreflightIssues; track issue) {
|
||||
<li>{{ issue }}</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (build.error_message) {
|
||||
<p class="status error"><strong>Error:</strong> {{ build.error_message }}</p>
|
||||
}
|
||||
|
||||
<h4>Artifacts</h4>
|
||||
<div class="artifact-list">
|
||||
@for (artifact of build.artifacts; track artifact.id) {
|
||||
<div class="artifact-row">
|
||||
<div>
|
||||
<strong>{{ artifact.file_name }}</strong>
|
||||
<div class="artifact-meta">{{ artifact.platform }} • {{ artifact.artifact_type }} • {{ artifact.sha256.slice(0, 12) }}…</div>
|
||||
</div>
|
||||
<a class="btn-icon btn-edit" [href]="getArtifactDownloadHref(artifact.download_url)">Download</a>
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="empty-row">No artifacts available yet.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>Build Log</h4>
|
||||
<pre class="build-log">{{ build.build_log || 'No logs yet.' }}</pre>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Stats Tab -->
|
||||
@if (activeTab() === 'stats') {
|
||||
<div class="tab-content">
|
||||
|
||||
@@ -408,3 +408,143 @@
|
||||
color: $neutral-white;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mobile builds tab ───────────────────────────────────
|
||||
|
||||
.mobile-builds-panel {
|
||||
display: grid;
|
||||
gap: $spacing-lg;
|
||||
}
|
||||
|
||||
.mobile-build-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.build-card {
|
||||
background: $neutral-white;
|
||||
border: 1px solid $neutral-light;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md;
|
||||
|
||||
h3,
|
||||
h4 {
|
||||
color: $primary-blue;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.derived-list {
|
||||
display: grid;
|
||||
gap: $spacing-xs;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: $neutral-dark;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='file'] {
|
||||
border: 1px solid $neutral-light;
|
||||
border-radius: $radius-sm;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
}
|
||||
|
||||
.platform-row {
|
||||
grid-template-columns: auto auto;
|
||||
|
||||
label {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
border-radius: $radius-sm;
|
||||
padding: $spacing-sm;
|
||||
margin: $spacing-sm 0;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&.success {
|
||||
background: rgba($success-green, 0.1);
|
||||
color: $success-green;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba($danger-red, 0.1);
|
||||
color: $danger-red;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: $spacing-xs 0 0 $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
.selected-build {
|
||||
background: rgba($primary-blue, 0.06);
|
||||
}
|
||||
|
||||
.tab-toolbar.compact {
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
.artifact-list {
|
||||
display: grid;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.artifact-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid $neutral-light;
|
||||
border-radius: $radius-sm;
|
||||
padding: $spacing-sm;
|
||||
}
|
||||
|
||||
.artifact-meta {
|
||||
font-size: 0.78rem;
|
||||
color: $neutral-medium;
|
||||
}
|
||||
|
||||
.build-log {
|
||||
background: $neutral-black;
|
||||
color: $neutral-white;
|
||||
border-radius: $radius-sm;
|
||||
padding: $spacing-sm;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@include responsive(md) {
|
||||
.mobile-build-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,16 @@ import { FormsModule } from '@angular/forms';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
|
||||
import { Show } from '../interfaces/show';
|
||||
import { Event } from '../interfaces/event';
|
||||
import { Event as StationEvent } from '../interfaces/event';
|
||||
import { StationConfig } from '../interfaces/station-config';
|
||||
import { HistoryEntry } from '../interfaces/history-entry';
|
||||
import { TeamMember } from '../interfaces/team-member';
|
||||
import { CommunityHighlight } from '../interfaces/community-highlight';
|
||||
import { Underwriter } from '../interfaces/underwriter';
|
||||
import {
|
||||
MobileBuildDefaults,
|
||||
MobileBuildRequest,
|
||||
} from '../interfaces/mobile-build';
|
||||
import { ShowService } from '../services/show.service';
|
||||
import { EventService } from '../services/event.service';
|
||||
import { StationConfigService } from '../services/station-config.service';
|
||||
@@ -18,6 +22,7 @@ import { HistoryEntryService } from '../services/history-entry.service';
|
||||
import { TeamMemberService } from '../services/team-member.service';
|
||||
import { CommunityHighlightService } from '../services/community-highlight.service';
|
||||
import { UnderwriterService } from '../services/underwriter.service';
|
||||
import { MobileBuildService } from '../services/mobile-build.service';
|
||||
import { AdminShowFormComponent } from './admin-show-form.component';
|
||||
import { AdminEventFormComponent } from './admin-event-form.component';
|
||||
import { AdminStationFormComponent } from './admin-station-form.component';
|
||||
@@ -38,8 +43,9 @@ import {
|
||||
HarmonyRule,
|
||||
ColorValidation,
|
||||
} from '../utils/color-harmony';
|
||||
import { getAppConfig } from '../services/app-config.service';
|
||||
|
||||
type TabKey = 'shows' | 'events' | 'station' | 'theme' | 'history' | 'team' | 'community' | 'underwriters' | 'stats';
|
||||
type TabKey = 'shows' | 'events' | 'station' | 'theme' | 'history' | 'team' | 'community' | 'underwriters' | 'mobile-builds' | 'stats';
|
||||
|
||||
interface ThemeColorState {
|
||||
color_primary: string;
|
||||
@@ -90,12 +96,13 @@ export class AdminComponent implements OnInit {
|
||||
private teamMemberService = inject(TeamMemberService);
|
||||
private communityHighlightService = inject(CommunityHighlightService);
|
||||
private underwriterService = inject(UnderwriterService);
|
||||
private mobileBuildService = inject(MobileBuildService);
|
||||
|
||||
activeTab = signal<TabKey>('shows');
|
||||
|
||||
/** Item currently being edited (set by editXxx, cleared on save/close). */
|
||||
editingShow: Show | null = null;
|
||||
editingEvent: Event | null = null;
|
||||
editingEvent: StationEvent | null = null;
|
||||
editingHistory: HistoryEntry | null = null;
|
||||
editingTeam: TeamMember | null = null;
|
||||
editingCommunity: CommunityHighlight | null = null;
|
||||
@@ -103,12 +110,34 @@ export class AdminComponent implements OnInit {
|
||||
|
||||
/** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */
|
||||
readonly shows$ = new BehaviorSubject<Show[]>([]);
|
||||
readonly events$ = new BehaviorSubject<Event[]>([]);
|
||||
readonly events$ = new BehaviorSubject<StationEvent[]>([]);
|
||||
readonly stationConfig$ = new BehaviorSubject<StationConfig | null>(null);
|
||||
readonly historyEntries$ = new BehaviorSubject<HistoryEntry[]>([]);
|
||||
readonly teamMembers$ = new BehaviorSubject<TeamMember[]>([]);
|
||||
readonly communityHighlights$ = new BehaviorSubject<CommunityHighlight[]>([]);
|
||||
readonly underwriters$ = new BehaviorSubject<Underwriter[]>([]);
|
||||
readonly mobileBuilds$ = new BehaviorSubject<MobileBuildRequest[]>([]);
|
||||
|
||||
mobileBuildDefaults: MobileBuildDefaults | null = null;
|
||||
selectedMobileBuildId: number | null = null;
|
||||
selectedMobileBuild: MobileBuildRequest | null = null;
|
||||
mobileBuildStatusMessage = '';
|
||||
mobileBuildErrorMessage = '';
|
||||
mobileBuildPreflightIssues: string[] = [];
|
||||
mobileBuildBusy = false;
|
||||
|
||||
buildAndroid = true;
|
||||
buildIos = true;
|
||||
androidApplicationId = '';
|
||||
iosBundleId = '';
|
||||
buildVersionName = '1.0.0';
|
||||
buildNumber = '1';
|
||||
buildReleaseProfile = 'release';
|
||||
|
||||
androidKeystoreFile: File | null = null;
|
||||
androidDescriptorFile: File | null = null;
|
||||
iosCertificateFile: File | null = null;
|
||||
iosProfileFile: File | null = null;
|
||||
|
||||
// ── Theme tab state ──────────────────────────────────────────
|
||||
|
||||
@@ -209,9 +238,184 @@ export class AdminComponent implements OnInit {
|
||||
this.reloadTeamMembers(),
|
||||
this.reloadCommunityHighlights(),
|
||||
this.reloadUnderwriters(),
|
||||
this.reloadMobileBuildDefaults(),
|
||||
this.reloadMobileBuilds(),
|
||||
]);
|
||||
}
|
||||
|
||||
async reloadMobileBuildDefaults(): Promise<void> {
|
||||
try {
|
||||
this.mobileBuildDefaults = await firstValueFrom(this.mobileBuildService.getDefaults());
|
||||
} catch {
|
||||
this.mobileBuildDefaults = null;
|
||||
}
|
||||
}
|
||||
|
||||
async reloadMobileBuilds(): Promise<void> {
|
||||
const data = await firstValueFrom(this.mobileBuildService.listRequests());
|
||||
this.mobileBuilds$.next(data);
|
||||
|
||||
if (this.selectedMobileBuildId !== null) {
|
||||
const selected = data.find((item) => item.id === this.selectedMobileBuildId) ?? null;
|
||||
this.selectedMobileBuild = selected;
|
||||
if (!selected) {
|
||||
this.selectedMobileBuildId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMobileBuildSelected(buildId: number): void {
|
||||
this.selectedMobileBuildId = buildId;
|
||||
const selected = this.mobileBuilds$.value.find((item) => item.id === buildId) ?? null;
|
||||
this.selectedMobileBuild = selected;
|
||||
this.mobileBuildPreflightIssues = selected?.preflight_report
|
||||
? selected.preflight_report.split('\n').filter((line) => line.trim().length > 0)
|
||||
: [];
|
||||
}
|
||||
|
||||
onFileChosen(event: Event, target: 'android-keystore' | 'android-descriptor' | 'ios-certificate' | 'ios-profile'): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files && input.files.length > 0 ? input.files[0] : null;
|
||||
|
||||
if (target === 'android-keystore') this.androidKeystoreFile = file;
|
||||
if (target === 'android-descriptor') this.androidDescriptorFile = file;
|
||||
if (target === 'ios-certificate') this.iosCertificateFile = file;
|
||||
if (target === 'ios-profile') this.iosProfileFile = file;
|
||||
}
|
||||
|
||||
private getErrorMessage(err: unknown, fallback: string): string {
|
||||
if (typeof err === 'object' && err !== null) {
|
||||
const maybeErr = err as { error?: { detail?: string | string[]; message?: string }; message?: string };
|
||||
const detail = maybeErr.error?.detail;
|
||||
if (typeof detail === 'string') return detail;
|
||||
if (Array.isArray(detail)) return detail.join(' | ');
|
||||
if (typeof maybeErr.error?.message === 'string') return maybeErr.error.message;
|
||||
if (typeof maybeErr.message === 'string') return maybeErr.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private resetMobileBuildMessages(): void {
|
||||
this.mobileBuildStatusMessage = '';
|
||||
this.mobileBuildErrorMessage = '';
|
||||
}
|
||||
|
||||
private validateBuildInputs(): string | null {
|
||||
if (!this.buildAndroid && !this.buildIos) {
|
||||
return 'Select at least one platform.';
|
||||
}
|
||||
|
||||
if (this.buildAndroid && !this.androidApplicationId.trim()) {
|
||||
return 'Android application ID is required.';
|
||||
}
|
||||
|
||||
if (this.buildIos && !this.iosBundleId.trim()) {
|
||||
return 'iOS bundle ID is required.';
|
||||
}
|
||||
|
||||
if (!this.buildVersionName.trim() || !this.buildNumber.trim()) {
|
||||
return 'Version name and build number are required.';
|
||||
}
|
||||
|
||||
if (this.buildAndroid && (!this.androidKeystoreFile || !this.androidDescriptorFile)) {
|
||||
return 'Android requires both keystore and descriptor files.';
|
||||
}
|
||||
|
||||
if (this.buildIos && (!this.iosCertificateFile || !this.iosProfileFile)) {
|
||||
return 'iOS requires both certificate and provisioning profile files.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async createAndUploadMobileBuildRequest(): Promise<void> {
|
||||
this.resetMobileBuildMessages();
|
||||
this.mobileBuildPreflightIssues = [];
|
||||
const inputError = this.validateBuildInputs();
|
||||
if (inputError) {
|
||||
this.mobileBuildErrorMessage = inputError;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mobileBuildBusy = true;
|
||||
try {
|
||||
const request = await firstValueFrom(this.mobileBuildService.createRequest({
|
||||
platform_android: this.buildAndroid,
|
||||
platform_ios: this.buildIos,
|
||||
android_application_id: this.androidApplicationId.trim() || undefined,
|
||||
ios_bundle_id: this.iosBundleId.trim() || undefined,
|
||||
version_name: this.buildVersionName.trim(),
|
||||
build_number: this.buildNumber.trim(),
|
||||
release_profile: this.buildReleaseProfile.trim() || 'release',
|
||||
}));
|
||||
|
||||
if (this.buildAndroid && this.androidKeystoreFile && this.androidDescriptorFile) {
|
||||
await firstValueFrom(this.mobileBuildService.uploadFile(request.id, 'android', 'keystore', this.androidKeystoreFile));
|
||||
await firstValueFrom(this.mobileBuildService.uploadFile(request.id, 'android', 'descriptor', this.androidDescriptorFile));
|
||||
}
|
||||
|
||||
if (this.buildIos && this.iosCertificateFile && this.iosProfileFile) {
|
||||
await firstValueFrom(this.mobileBuildService.uploadFile(request.id, 'ios', 'certificate', this.iosCertificateFile));
|
||||
await firstValueFrom(this.mobileBuildService.uploadFile(request.id, 'ios', 'profile', this.iosProfileFile));
|
||||
}
|
||||
|
||||
this.mobileBuildStatusMessage = `Draft build request #${request.id} created and files uploaded.`;
|
||||
await this.reloadMobileBuilds();
|
||||
this.onMobileBuildSelected(request.id);
|
||||
} catch (err) {
|
||||
this.mobileBuildErrorMessage = this.getErrorMessage(err, 'Failed to create build request.');
|
||||
} finally {
|
||||
this.mobileBuildBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async runPreflightForSelectedBuild(): Promise<void> {
|
||||
if (!this.selectedMobileBuildId) return;
|
||||
|
||||
this.resetMobileBuildMessages();
|
||||
this.mobileBuildBusy = true;
|
||||
try {
|
||||
const result = await firstValueFrom(this.mobileBuildService.preflight(this.selectedMobileBuildId));
|
||||
this.mobileBuildPreflightIssues = result.issues;
|
||||
this.mobileBuildStatusMessage = result.passed
|
||||
? 'Preflight passed. Build can be triggered.'
|
||||
: 'Preflight failed. Resolve the listed issues.';
|
||||
await this.reloadMobileBuilds();
|
||||
this.onMobileBuildSelected(this.selectedMobileBuildId);
|
||||
} catch (err) {
|
||||
this.mobileBuildErrorMessage = this.getErrorMessage(err, 'Preflight failed to run.');
|
||||
} finally {
|
||||
this.mobileBuildBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async triggerSelectedBuild(): Promise<void> {
|
||||
if (!this.selectedMobileBuildId) return;
|
||||
|
||||
this.resetMobileBuildMessages();
|
||||
this.mobileBuildBusy = true;
|
||||
try {
|
||||
const request = await firstValueFrom(this.mobileBuildService.trigger(this.selectedMobileBuildId));
|
||||
this.mobileBuildStatusMessage = `Build #${request.id} queued.`;
|
||||
await this.reloadMobileBuilds();
|
||||
this.onMobileBuildSelected(this.selectedMobileBuildId);
|
||||
} catch (err) {
|
||||
this.mobileBuildErrorMessage = this.getErrorMessage(err, 'Failed to trigger build.');
|
||||
} finally {
|
||||
this.mobileBuildBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSelectedBuild(): Promise<void> {
|
||||
if (!this.selectedMobileBuildId) return;
|
||||
await this.reloadMobileBuilds();
|
||||
this.onMobileBuildSelected(this.selectedMobileBuildId);
|
||||
}
|
||||
|
||||
getArtifactDownloadHref(path: string): string {
|
||||
return `${getAppConfig().apiBaseUrl}${path}`;
|
||||
}
|
||||
|
||||
// ── Show CRUD ───────────────────────────────────────────
|
||||
|
||||
openShowForm(): void {
|
||||
@@ -248,7 +452,7 @@ export class AdminComponent implements OnInit {
|
||||
this.showEventForm = true;
|
||||
}
|
||||
|
||||
editEvent(event: Event): void {
|
||||
editEvent(event: StationEvent): void {
|
||||
this.editingEvent = event;
|
||||
this.showEventForm = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
export interface MobileBuildDefaults {
|
||||
tenant_key: string;
|
||||
app_name: string;
|
||||
tagline: string;
|
||||
support_email: string;
|
||||
website: string;
|
||||
stream_url: string;
|
||||
stream_metadata_url: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildUploadedFile {
|
||||
id: number;
|
||||
request_id: number;
|
||||
platform: 'android' | 'ios';
|
||||
file_kind: string;
|
||||
original_name: string;
|
||||
size: number;
|
||||
uploaded_at: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildArtifact {
|
||||
id: number;
|
||||
request_id: number;
|
||||
platform: 'android' | 'ios';
|
||||
artifact_type: string;
|
||||
file_name: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
created_at: string;
|
||||
download_url: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildRequest {
|
||||
id: number;
|
||||
tenant_key: string;
|
||||
platform_android: boolean;
|
||||
platform_ios: boolean;
|
||||
android_application_id: string | null;
|
||||
ios_bundle_id: string | null;
|
||||
version_name: string;
|
||||
build_number: string;
|
||||
release_profile: string;
|
||||
status: string;
|
||||
preflight_passed: boolean;
|
||||
preflight_report: string;
|
||||
build_log: string;
|
||||
error_message: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
files: MobileBuildUploadedFile[];
|
||||
artifacts: MobileBuildArtifact[];
|
||||
}
|
||||
|
||||
export interface MobileBuildCreatePayload {
|
||||
platform_android: boolean;
|
||||
platform_ios: boolean;
|
||||
android_application_id?: string;
|
||||
ios_bundle_id?: string;
|
||||
version_name: string;
|
||||
build_number: string;
|
||||
release_profile: string;
|
||||
}
|
||||
|
||||
export interface MobileBuildPreflightResult {
|
||||
passed: boolean;
|
||||
issues: string[];
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { getAppConfig } from './app-config.service';
|
||||
import {
|
||||
MobileBuildCreatePayload,
|
||||
MobileBuildDefaults,
|
||||
MobileBuildPreflightResult,
|
||||
MobileBuildRequest,
|
||||
MobileBuildUploadedFile,
|
||||
} from '../interfaces/mobile-build';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MobileBuildService {
|
||||
private http = inject(HttpClient);
|
||||
private baseUrl = `${getAppConfig().apiBaseUrl}/api/mobile-builds`;
|
||||
|
||||
getDefaults(): Observable<MobileBuildDefaults> {
|
||||
return this.http.get<MobileBuildDefaults>(`${this.baseUrl}/defaults`);
|
||||
}
|
||||
|
||||
listRequests(): Observable<MobileBuildRequest[]> {
|
||||
return this.http.get<MobileBuildRequest[]>(`${this.baseUrl}/requests`);
|
||||
}
|
||||
|
||||
getRequest(id: number): Observable<MobileBuildRequest> {
|
||||
return this.http.get<MobileBuildRequest>(`${this.baseUrl}/requests/${id}`);
|
||||
}
|
||||
|
||||
createRequest(payload: MobileBuildCreatePayload): Observable<MobileBuildRequest> {
|
||||
return this.http.post<MobileBuildRequest>(`${this.baseUrl}/requests`, payload);
|
||||
}
|
||||
|
||||
uploadFile(
|
||||
requestId: number,
|
||||
platform: 'android' | 'ios',
|
||||
fileKind: string,
|
||||
file: File,
|
||||
): Observable<MobileBuildUploadedFile> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const params = new HttpParams()
|
||||
.set('platform', platform)
|
||||
.set('file_kind', fileKind);
|
||||
|
||||
return this.http.post<MobileBuildUploadedFile>(
|
||||
`${this.baseUrl}/requests/${requestId}/files`,
|
||||
form,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
preflight(requestId: number): Observable<MobileBuildPreflightResult> {
|
||||
return this.http.post<MobileBuildPreflightResult>(
|
||||
`${this.baseUrl}/requests/${requestId}/preflight`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
trigger(requestId: number): Observable<MobileBuildRequest> {
|
||||
return this.http.post<MobileBuildRequest>(
|
||||
`${this.baseUrl}/requests/${requestId}/trigger`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user