| | |
| | | export interface Detection { class_id: number; class_name: string; confidence: number; bbox_xyxy: number[]; } |
| | | export interface GeoFeature { type?: string; properties?: Record<string, unknown>; geometry?: { type?: string; coordinates?: unknown }; } |
| | | export interface DetectionImage { file: string; annotated_file: string; width: number; height: number; detections: Detection[]; } |
| | | export interface DetectionRun { detection_count: number; processed_images: number; elapsed_seconds: number; device: string; model: string; confidence: number; input_dir: string; notes: string[]; created_at?: string; } |
| | | export interface DetectionCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; createdAt: string; run: DetectionRun; images: DetectionImage[]; } |
| | |
| | | export interface MeasurementImage { file: string; width: number; height: number; raster_file: string; preview_file: string; vector_file: string; csv_file: string; object_count: number; class_counts: Record<string, number>; total_area: number; total_perimeter: number; area_unit: string; length_unit: string; measurement_basis: string; crs: string | null; georeferenced: boolean; vectorizer: string; elapsed_seconds: number; } |
| | | export interface MeasurementRun { capability: string; classification: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; input_count: number; processed_images: number; elapsed_seconds: number; images: MeasurementImage[]; limitations: string[]; input_dir?: string; raw_input_dir?: string; } |
| | | export interface MeasurementCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: MeasurementRun; } |
| | | export interface PointCloudImage { file: string; original_points: number; downsampled_points: number; ground_inliers: number; elevated_points: number; elevated_point_ratio: number; elevated_footprint_count: number; mesh_triangles: number; mesh_skipped_for_large_cloud?: boolean; raster_width: number; raster_height: number; coordinate_basis: string; crs: string | null; classified_point_cloud: string; semantic_preview_point_cloud?: string; semantic_annotation_source_point_cloud?: string; semantic_preview_points?: number; semantic_classified_las?: string; semantic_raster_file?: string; semantic_preview_file?: string; semantic_vector_file?: string; semantic_summary_file?: string; semantic_footprint_count?: number; semantic_class_counts?: Record<string, number>; semantic_source_has_rgb?: boolean; semantic_method?: string; mesh_file: string | null; dsm_file: string; height_file: string; label_raster_file: string; preview_file: string; vector_file: string; summary_file: string; vectorizer: string; elapsed_seconds: number; } |
| | | export interface PhotoReconstruction { input_images: number; registered_images: number; unregistered_images: string[]; sparse_points: number; mean_reprojection_error_pixels: number; camera_count: number; coordinate_basis: string; point_cloud_file: string; preview_file: string; camera_pose_file: string; input_manifest_file: string; colmap_model_directory: string; elapsed_seconds: number; initial_min_triangulation_angle_degrees: number; } |
| | | export interface DensePhotoReconstruction { input_images: number; registered_images: number; sparse_points: number; dense_points: number; mesh_vertices: number; mesh_faces: number; preview_mesh_faces?: number; preview_mesh_face_ratio?: number; geometry_preview_faces?: number; point_color_preview_faces?: number; coordinate_basis: string; dense_point_cloud_file: string; mesh_file: string; textured_model_file: string; geometry_preview_model_file?: string; point_color_preview_model_file?: string; texture_preview_filter_report?: string; texture_file: string; elapsed_seconds: number | null; } |
| | | export interface PointCloudRun { capability: string; classification: string; created_at?: string; versions: Record<string, string>; method: string; model: string; device: string; thresholds: Record<string, number | string>; input_count?: number; processed_point_clouds?: number; elapsed_seconds: number; point_clouds?: PointCloudImage[]; photo_reconstruction?: PhotoReconstruction; dense_photo_reconstruction?: DensePhotoReconstruction; limitations: string[]; raw_input_dir?: string; } |
| | | export interface PointCloudCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: PointCloudRun; features: GeoFeature[]; } |
| | | export interface RiskScoreFeature { type: "Feature"; properties: { object_id: string; confidence: number; raw_score: number; risk_score: number; risk_level: "low" | "medium" | "high" | "critical"; score_capped: boolean; hit_rule_ids: string[]; suggestions: string[]; rule_contributions: Array<{ rule_id: string; score: number; suggestion: string }>; }; geometry: { type: string; coordinates: number[] }; } |
| | | export interface RiskSummary { object_count: number; risk_level_counts: Record<"low" | "medium" | "high" | "critical", number>; maximum_score: number; mean_score: number; hit_rule_counts: Record<string, number>; } |
| | | export interface RiskRuleRun { schema_version: number; capability: string; classification: string; created_at?: string; method: string; model: string; device: string; geoai_role: string; crs: string; zones_reprojected_to_observation_crs: boolean; parameters: { score_cap: number; levels: Record<string, number>; raster_resolution: number; rule_count: number; }; raster: { width: number; height: number; bounds: number[]; coordinate_basis: string; }; summary: RiskSummary; artifacts: { risk_raster: string; risk_preview: string; risk_vector: string; risk_scores_csv: string; summary: string; }; elapsed_seconds: number; limitations: string[]; } |
| | | export interface RiskRuleCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: RiskRuleRun; features: RiskScoreFeature[]; } |
| | | export interface AnomalyCandidate { type: "Feature"; properties: { feature_id?: number; mask_code?: number; method?: string; rule_flag?: boolean; isolation_flag?: boolean; agreement?: boolean; max_rule_score?: number; max_isolation_score?: number; reason_feature?: string; area_pixels?: number }; geometry: { type: string; coordinates: unknown }; } |
| | | export interface AnomalyImage { file: string; width: number; height: number; tile_count: number; rule_heatmap_file: string; isolation_heatmap_file: string; overlay_file: string; mask_file: string; vector_file: string; tiles_file: string; candidate_count: number; rule_anomaly_pixels: number; isolation_anomaly_pixels: number; agreement_pixels: number; rule_anomaly_coverage: number; isolation_anomaly_coverage: number; agreement_coverage: number; georeferenced: boolean; crs: string | null; coordinate_basis: string; vectorizer: string; elapsed_seconds: number; } |
| | | export interface AnomalyImage { file: string; width: number; height: number; tile_count: number; rule_heatmap_file: string; isolation_heatmap_file: string; overlay_file: string; mask_file: string; vector_file: string; tiles_file: string; candidate_count: number; rule_anomaly_pixels: number; local_change_pixels?: number; isolation_anomaly_pixels: number; agreement_pixels: number; rule_anomaly_coverage: number; local_change_coverage?: number; isolation_anomaly_coverage: number; agreement_coverage: number; georeferenced: boolean; crs: string | null; coordinate_basis: string; vectorizer: string; elapsed_seconds: number; } |
| | | export interface AnomalyRun { capability: string; classification: string; created_at?: string; versions: Record<string, string>; method: string; model: string; device: string; reference_count: number; reference_tile_count: number; input_count: number; reference_images: Array<{ file: string; tile_count: number }>; feature_names: string[]; parameters: { tile_size: number; stride: number; threshold_quantile: number; random_state: number; spatial_mode_requested?: "auto" | "global" | "aligned"; rule_mode_selected?: "global" | "aligned" }; thresholds: { rule_score: number; isolation_score: number }; images: AnomalyImage[]; elapsed_seconds: number; limitations: string[]; } |
| | | export interface AnomalyCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; referenceRoot: string; createdAt: string; run: AnomalyRun; candidates: Record<string, AnomalyCandidate[]>; } |
| | | export interface ChangeFeature { type: "Feature"; properties: { feature_id?: number; area_pixels?: number; mean_probability?: number; max_probability?: number; bounds_pixel?: number[]; confidence?: number; class?: number }; geometry: { type: string; coordinates: unknown }; } |
| | | export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; overlay: string; vector: string; features: string }; created_at?: string; } |
| | | export interface ChangeCase { id: string; label: string; note: string; artifactRoot: string; beforeImage: string; afterImage: string; createdAt: string; run: ChangeRun; features: ChangeFeature[]; } |
| | | export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; rectangle_feature_count?: number; generic_difference?: { candidate_pixels: number; candidate_pixel_ratio: number; components_after_filter: number }; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; generic_difference_mask?: string; overlay: string; model_overlay?: string; before_processed_preview?: string; after_registered_preview?: string; vector: string; rectangle_vector?: string; rectangle_vector_wgs84?: string | null; features: string }; created_at?: string; } |
| | | export interface ChangeCase { id: string; label: string; note: string; artifactRoot: string; beforeImage: string; afterImage: string; rawAfterImage?: string; registeredBeforeImage?: string; registeredAfterImage?: string; createdAt: string; run: ChangeRun; features: ChangeFeature[]; } |
| | | export interface ChangeScanResult { id: string; label: string; threshold: number; minimumAreaPixels: number; cleanedComponents: number; changedPixels: number; changedPixelRatio: number; vectorFeatureCount: number; fullVectorFeatureCount?: number | null; rectangleFeatureCount?: number | null; overlay: string; mask: string; regions: string; vector?: string | null; rectangleVector?: string | null; rectangleVectorWgs84?: string | null; } |
| | | export interface ChangeParameterScan { id: string; label: string; note: string; artifactRoot: string; sourceRun?: string; userSubmitted?: boolean; contactSheet?: string | null; results: ChangeScanResult[]; } |
| | | export interface ChangeScanJob { id: string; status: "queued" | "running" | "completed" | "failed"; phase?: "inference" | "parameter-scan" | "vectorization" | "done" | "error"; scanId?: string; error?: string; thresholds?: number[]; minimumAreas?: number[]; } |
| | | export interface TrajectoryEvent { event_id: string; event_type: string; track_ids: string[]; start_time: string; end_time: string; duration_seconds: number; } |
| | | export interface TrajectoryCaseRun { case_id: string; input_count: number; track_count: number; event_count: number; dropped_duplicate_observations: number; elapsed_seconds: number; device: string; input: string; thresholds: Record<string, number>; created_at?: string; } |
| | | export interface TrajectorySummary { track_id: string; entity_type: string; point_count: string; distance_m: string; average_speed_mps: string; behavior_labels: string; } |
| | |
| | | interface DetectionDefinition extends CaseDefinition { inputRoot: string; } |
| | | interface SemanticDefinition extends CaseDefinition { inputRoot: string; rawInputRoot: string; } |
| | | interface MeasurementDefinition extends CaseDefinition {} |
| | | interface PointCloudDefinition extends CaseDefinition {} |
| | | interface RiskRuleDefinition extends CaseDefinition {} |
| | | interface ChangeDefinition extends CaseDefinition { beforeImage: string; afterImage: string; } |
| | | interface AnomalyDefinition extends CaseDefinition { inputRoot: string; referenceRoot: string; } |
| | | export interface UploadFilePayload { name: string; content: string; } |
| | | export interface ChangeUploadRef { uploadId: string; role: "before" | "after"; name: string; size: number; } |
| | | export interface AnomalyUploadRef { uploadId: string; role: "reference" | "input"; name: string; size: number; sha256: string; } |
| | | export interface AnomalyJob { id: string; runId: string; status: "queued" | "running" | "complete" | "failed"; createdAt: string; finishedAt?: string; error?: string; run?: AnomalyDefinition; } |
| | | export interface PhotoReconstructionUploadRef { uploadId: string; role: "photo"; name: string; size: number; sha256: string; } |
| | | export interface PointCloudUploadRef { uploadId: string; role: "pointcloud"; name: string; size: number; sha256: string; } |
| | | export interface PhotoReconstructionJob { id: string; runId: string; inputImages: number; usePositionPriors: boolean; status: "queued" | "running" | "complete" | "failed"; stage: "queued" | "sparse_sfm" | "dense_mvs" | "complete" | "failed"; createdAt: string; startedAt?: string; finishedAt?: string; error?: string; run?: PointCloudDefinition; } |
| | | export interface PointCloudAnnotationSource { id: string; runId: string; label: string; artifactRoot: string; file: string; url: string; sha256: string; pointCount: number; sourceKind: string; } |
| | | export interface PointCloudAnnotation { id: string; sourceId: string; createdAt: string; labelCount: number; classCounts: Record<string, number>; path: string; } |
| | | export interface PointCloudTrainingJob { id: string; annotationId: string; status: "queued" | "running" | "complete" | "failed"; stage: string; device: string; createdAt: string; error?: string; artifactRoot?: string; metrics?: string; model?: string; preview?: string; } |
| | | export interface PointCloudSemanticModel { id: string; label: string; artifactRoot: string; model: string; metrics: string; createdAt: string; classes: Record<string, { key: string; label: string; color: number[] }>; testF1: Record<string, number>; } |
| | | export interface PointCloudInferenceJob { id: string; runId: string; modelId: string; inputName: string; status: "queued" | "running" | "complete" | "failed"; stage: string; device: string; createdAt: string; error?: string; artifactRoot?: string; metadata?: string; preview?: string; classifiedLas?: string; classCounts?: string; summary?: string; } |
| | | |
| | | export const artifactUrl = (path: string) => `/${path.replace(/\\/g, "/").split("/").map(encodeURIComponent).join("/")}`; |
| | | |
| | |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | |
| | | export async function loadPointCloudArtifacts(): Promise<Record<string, PointCloudCase>> { |
| | | const { runs } = await getJson<{ runs: PointCloudDefinition[] }>("api/3d-pointcloud/runs"); |
| | | const cases = await Promise.all(runs.map(async (definition) => { |
| | | const run = await getJson<PointCloudRun>(`${definition.artifactRoot}/run_metadata.json`); |
| | | const features = (await Promise.all((run.point_clouds ?? []).map(async (item) => { |
| | | const payload = await getJson<{ features?: GeoFeature[] }>(`${definition.artifactRoot}/${item.vector_file}`); |
| | | return payload.features ?? []; |
| | | }))).flat(); |
| | | return { ...definition, run, features } satisfies PointCloudCase; |
| | | })); |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | |
| | | export async function loadRiskRuleArtifacts(): Promise<Record<string, RiskRuleCase>> { |
| | | const { runs } = await getJson<{ runs: RiskRuleDefinition[] }>("api/risk-rule-engine/runs"); |
| | | const cases = await Promise.all(runs.map(async (definition) => { |
| | | const run = await getJson<RiskRuleRun>(`${definition.artifactRoot}/run_metadata.json`); |
| | | const vector = await getJson<{ features?: RiskScoreFeature[] }>(`${definition.artifactRoot}/${run.artifacts.risk_vector}`); |
| | | return { ...definition, run, features: vector.features ?? [] } satisfies RiskRuleCase; |
| | | })); |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | |
| | | export async function loadChangeArtifacts(): Promise<Record<string, ChangeCase>> { |
| | | const { runs } = await getJson<{ runs: ChangeDefinition[] }>("api/change-detection/runs"); |
| | | const cases = await Promise.all(runs.map(async (definition) => { |
| | | const run = await getJson<ChangeRun>(`${definition.artifactRoot}/run_metadata.json`); |
| | | const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.vector}`); |
| | | const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.rectangle_vector || run.artifacts.vector}`); |
| | | return { ...definition, run, features: vector.features ?? [] } satisfies ChangeCase; |
| | | })); |
| | | return Object.fromEntries(cases.map((item) => [item.id, item])); |
| | | } |
| | | export async function loadChangeParameterScans(): Promise<ChangeParameterScan[]> { |
| | | return (await getJson<{ scans: ChangeParameterScan[] }>("api/change-detection/scans")).scans; |
| | | } |
| | | |
| | | export async function loadAnomalyArtifacts(): Promise<Record<string, AnomalyCase>> { |
| | |
| | | export async function createDetectionRun(images: UploadFilePayload[]) { return postRun<{ run: DetectionDefinition }>("api/object-detection/runs", { images }); } |
| | | export async function createSemanticRun(images: UploadFilePayload[], taskId: string) { return postRun<{ run: SemanticDefinition }>("api/semantic-mapping/runs", { images, taskId }); } |
| | | export async function createMeasurementRun(rasters: UploadFilePayload[]) { return postRun<{ run: MeasurementDefinition }>("api/spatial-measurement/runs", { rasters }); } |
| | | export async function createPointCloudRun(pointClouds: Array<UploadFilePayload | PointCloudUploadRef>) { return postRun<{ run: PointCloudDefinition }>("api/3d-pointcloud/runs", { pointClouds }); } |
| | | export async function createDensePointCloudClassification(sourceDenseRunId: string) { return postRun<{ run: PointCloudDefinition }>("api/3d-pointcloud/runs", { sourceDenseRunId }); } |
| | | export async function uploadPointCloudFile(file: File): Promise<PointCloudUploadRef> { |
| | | const uploadId = globalThis.crypto.randomUUID().replaceAll("-", ""); |
| | | const response = await fetch(`/api/3d-pointcloud/pointcloud-uploads/${uploadId}?role=pointcloud`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file }); |
| | | return responseJson<PointCloudUploadRef>(response); |
| | | } |
| | | export async function uploadPhotoReconstructionFile(file: File): Promise<PhotoReconstructionUploadRef> { |
| | | const uploadId = globalThis.crypto.randomUUID().replaceAll("-", ""); |
| | | const response = await fetch(`/api/3d-pointcloud/photo-uploads/${uploadId}?role=photo`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file }); |
| | | return responseJson<PhotoReconstructionUploadRef>(response); |
| | | } |
| | | export async function createPhotoReconstructionRun(photos: PhotoReconstructionUploadRef[], usePositionPriors: boolean) { return postRun<{ job: PhotoReconstructionJob }>("api/3d-pointcloud/photo-reconstruction-runs", { photos, usePositionPriors }); } |
| | | export async function loadPhotoReconstructionJob(jobId: string) { return (await getJson<{ job: PhotoReconstructionJob }>(`api/3d-pointcloud/photo-reconstruction-jobs/${jobId}`)).job; } |
| | | export async function loadPointCloudAnnotationSources() { return (await getJson<{ sources: PointCloudAnnotationSource[] }>("api/3d-pointcloud/annotation-sources")).sources; } |
| | | export async function loadPointCloudAnnotations() { return (await getJson<{ annotations: PointCloudAnnotation[] }>("api/3d-pointcloud/annotations")).annotations; } |
| | | export async function createPointCloudAnnotation(sourceId: string, labels: Array<[number, number]>) { return postRun<{ annotation: PointCloudAnnotation }>("api/3d-pointcloud/annotations", { sourceId, labels }); } |
| | | export async function deletePointCloudAnnotation(annotationId: string) { return responseJson<{ deletedId: string }>(await fetch(artifactUrl(`api/3d-pointcloud/annotations/${encodeURIComponent(annotationId)}`), { method: "DELETE" })); } |
| | | export async function createPointCloudTrainingRun(annotationId: string, device: "auto" | "cpu" | "cuda" = "cpu") { return postRun<{ job: PointCloudTrainingJob }>("api/3d-pointcloud/training-runs", { annotationId, device }); } |
| | | export async function loadPointCloudTrainingJob(jobId: string) { return (await getJson<{ job: PointCloudTrainingJob }>(`api/3d-pointcloud/training-jobs/${jobId}`)).job; } |
| | | export async function loadPointCloudSemanticModels() { return (await getJson<{ models: PointCloudSemanticModel[] }>("api/3d-pointcloud/semantic-models")).models; } |
| | | export async function createPointCloudModelInference(modelId: string, pointCloud: PointCloudUploadRef) { return postRun<{ job: PointCloudInferenceJob }>("api/3d-pointcloud/model-inference-runs", { modelId, pointCloud }); } |
| | | export async function loadPointCloudModelInferenceJob(jobId: string) { return (await getJson<{ job: PointCloudInferenceJob }>(`api/3d-pointcloud/model-inference-jobs/${jobId}`)).job; } |
| | | export async function createRiskRuleRun(files: { observations: UploadFilePayload; zones: UploadFilePayload; rules: UploadFilePayload }) { return postRun<{ run: RiskRuleDefinition }>("api/risk-rule-engine/runs", { files }); } |
| | | export async function createChangeRun(files: { before: UploadFilePayload; after: UploadFilePayload }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { files, threshold, maxDimension, processingMode }); } |
| | | export async function uploadChangeFile(file: File, role: "before" | "after"): Promise<ChangeUploadRef> { |
| | | const uploadId = globalThis.crypto.randomUUID().replaceAll("-", ""); |
| | | const query = new URLSearchParams({ role }); |
| | | // HTTP header values are restricted to ISO-8859-1 by browsers. Percent-encode |
| | | // the original name so Chinese filenames do not fail before the upload starts. |
| | | const response = await fetch(`/api/change-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file }); |
| | | return responseJson<ChangeUploadRef>(response); |
| | | } |
| | | export async function createChangeRunFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { uploads, threshold, maxDimension, processingMode }); } |
| | | export async function createChangeScanFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, thresholds: number[], minimumAreas: number[], maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ job: ChangeScanJob }>("api/change-detection/scans", { uploads, thresholds, minimumAreas, maxDimension, processingMode }); } |
| | | export async function getChangeScanJob(jobId: string) { return (await getJson<{ job: ChangeScanJob }>(`api/change-detection/scan-jobs/${jobId}`)).job; } |
| | | export async function promoteChangeScan(scanId: string, resultId: string) { return postRun<{ run: ChangeDefinition }>(`api/change-detection/scans/${encodeURIComponent(scanId)}/promote`, { resultId }); } |
| | | export async function uploadAnomalyFile(file: File, role: "reference" | "input"): Promise<AnomalyUploadRef> { |
| | | const uploadId = globalThis.crypto.randomUUID().replaceAll("-", ""); |
| | | const query = new URLSearchParams({ role }); |