Vibe json data export

This commit is contained in:
2026-09-30 01:12:08 -04:00
parent 3d5d1d1acd
commit 25506b4b87
8 changed files with 464 additions and 226 deletions
+123
View File
@@ -0,0 +1,123 @@
---
sessionId: session-260929-233614-1hed
---
# Requirements
### Overview & Goals
Add a "Download JSON" button to the interactive Galaxy Map in `ZeroSpace.Web` that exports all data powering the galaxy map into a structured, downloadable JSON file (`galaxy-map-data.json`). The exported data will include all planetary systems with their full metadata and Markdown bodies, system masteries with their bonuses and notes, and planetary mastery items.
### Scope
- **In Scope**:
- A client-side browser file download utility via Blazor `IJSRuntime`.
- Aggregation and serialization of `StarSystems.All` (including all markdown notes in `Body`), `SystemMasteries.All`, and `PlanetaryMasteryItems.All`.
- A "Download JSON" button styled with the project's Sci-Fi theme in `ZeroSpace.Web/Pages/Galaxy.razor`.
- **Out of Scope**:
- Backend API changes (the export is generated client-side from the static/in-memory data models already present in Blazor WebAssembly).
- Editing or mutating star system data.
### User Stories
- **As a player / community developer**, I want to download all galaxy map data (including star systems, hyperlane connections, occupation buffs, masteries, and markdown notes) in JSON format from the Galaxy Map page so that I can analyze the data offline, create custom tooling, or reference game lore.
### Functional Requirements
1. **Download Action**: Clicking the "Download JSON" button generates and initiates a browser download of `galaxy-map-data.json`.
2. **Star System Data & Markdown**: The JSON output must contain all star systems from `StarSystems.All` including ID, name, category, owner, yield flags, occupation buff, system mastery, connected systems, coordinates, and full markdown text (`Body`).
3. **Mastery Data**: The JSON output must contain all system masteries from `SystemMasteries.All` (levels, available systems, stats, and markdown body) and planetary mastery items from `PlanetaryMasteryItems.All`.
4. **JSON Formatting**: The output JSON must be well-formatted (indented, camelCase property naming, string representations for enums).
5. **UI Integration**: The button must be positioned intuitively in the Galaxy header / control bar matching the existing glassmorphic Sci-Fi styling.
### Non-Functional Requirements
- **Performance**: Export generation and download initiation should take < 50ms without blocking UI interactions.
- **Offline / WebAssembly Compatibility**: The export must function entirely within the client-side Blazor WebAssembly runtime without requiring server endpoints.
# Technical Design
### Current Implementation
- `ZeroSpace.Web/Pages/Galaxy.razor`: The interactive galaxy map page displaying star systems, hyperlanes, and faction occupation details using `StarSystems.All` and `SystemMasteries.All`.
- `ZeroSpace.Model/StarSystemData.cs` & `StarSystems.g.cs`: Contains `StarSystemData` records where `Body` contains the planetary system Markdown notes.
- `ZeroSpace.Model/SystemMasteryData.cs` & `SystemMasteries.g.cs`: Contains `SystemMasteryData` records with mastery levels, system associations, stats, and markdown `Body`.
- `ZeroSpace.Model/PlanetaryMasteryItemData.cs` & `PlanetaryMasteryItems.g.cs`: Contains `PlanetaryMasteryItemData` records for mastery items.
### Key Decisions
1. **Client-Side Export**: Use Blazor WebAssembly's `IJSRuntime` with a lightweight browser Blob URL helper to trigger file download instantly in memory without backend roundtrips.
2. **Unified Data Structure**: Group data under top-level properties (`systems`, `masteries`, `planetaryMasteryItems`, `exportedAt`) for easy consumption by external tools.
3. **Formatting**: Use `System.Text.Json` with `JsonSerializerOptions` configured for `WriteIndented = true`, `PropertyNamingPolicy = JsonNamingPolicy.CamelCase`, and `JsonStringEnumConverter`.
### Proposed Changes
1. **File Download Helper**:
- Add `ZeroSpace.Web/wwwroot/js/fileDownload.js` containing `downloadFileFromText(fileName, contentType, textContent)`.
- Include script reference in `ZeroSpace.Web/wwwroot/index.html`.
2. **Export Model**:
- Define `GalaxyMapExportData` record containing `IReadOnlyList<StarSystemData> Systems`, `IReadOnlyList<SystemMasteryData> Masteries`, `IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems`, and `DateTimeOffset ExportedAt`.
3. **Galaxy Page Update**:
- Add a "Download JSON" button to the header action bar in `Galaxy.razor`.
- Add `DownloadJsonAsync()` method in `@code` section that builds `GalaxyMapExportData`, serializes to JSON, and calls the JS helper.
### Data Models / Contracts
```csharp
public sealed record GalaxyMapExportData(
DateTimeOffset ExportedAt,
int TotalSystems,
int TotalMasteries,
IReadOnlyList<StarSystemData> Systems,
IReadOnlyList<SystemMasteryData> Masteries,
IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems);
```
### Architecture Diagram
```mermaid
graph LR
User[User clicks Download JSON] --> Razor[Galaxy.razor]
Razor --> Aggregator[Aggregate StarSystems, Masteries, Items]
Aggregator --> Serializer[System.Text.Json Serializer]
Serializer --> JSInterop[IJSRuntime downloadFileFromText]
JSInterop --> Browser[Browser saves galaxy-map-data.json]
```
### File Structure
- `ZeroSpace.Web/wwwroot/js/fileDownload.js` *(new)*
- `ZeroSpace.Web/wwwroot/index.html` *(modified to include script tag)*
- `ZeroSpace.Web/Models/GalaxyMapExportData.cs` *(new)*
- `ZeroSpace.Web/Pages/Galaxy.razor` *(modified to add download button and handler)*
- `ZeroSpace.Web/Pages/Galaxy.razor.css` *(optional style tweaks for button layout)*
### Risks
- **Large Payload / Memory**: The total size of all systems and markdown is < 500 KB, which fits easily in browser memory for Blob generation.
# Testing
### Validation Approach
- Verify solution builds with 0 errors via `build_solution_start` and `build_solution_state`.
- Verify the serialized JSON structure against required fields (star system metadata, markdown `Body`, masteries, levels, stats).
- Verify the button UI placement, styling, and JSInterop invocation logic.
### Key Scenarios
1. **Download Button Interaction**: User clicks the "Download JSON" button on `/galaxy`.
- Expected: `downloadFileFromText` JSInterop is called with `galaxy-map-data.json`, `application/json`, and the formatted JSON string.
2. **JSON Content Completeness**:
- Every star system from `StarSystems.All` is present in the `systems` array.
- The markdown `body` field of each star system is included.
- Every mastery from `SystemMasteries.All` is present in the `masteries` array with levels and stats.
- Planetary mastery items from `PlanetaryMasteryItems.All` are included.
### Edge Cases
- Systems with empty vs populated markdown `Body` serialize properly as strings (`""` or markdown text).
- Enum values (`StarSystemCategory`) serialize cleanly as strings (e.g. `"homeworldSystem"`, `"coreSystem"`, `"sideSystem"` or PascalCase).
# Delivery Steps
### ✓ Step 1: Implement file download JSInterop helper and export serialization model
Create the file download JSInterop utility and the galaxy map data export serialization contract.
- Add a JavaScript file download helper (`ZeroSpace.Web/wwwroot/js/fileDownload.js` or equivalent) that accepts filename, content type, and content/bytes, creating an object URL blob and triggering a download anchor click.
- Reference the script in `ZeroSpace.Web/wwwroot/index.html`.
- Define an export container record (e.g., `GalaxyMapExportData`) in `ZeroSpace.Web` / `ZeroSpace.Model` that aggregates star systems (with full metadata and markdown `Body`), system masteries (with levels, stats, available systems, and `Body`), and planetary mastery items.
- Implement a JSON serialization helper/service configured with `JsonSerializerOptions` (`WriteIndented = true`, camelCase naming, `JsonStringEnumConverter`) for clean formatting.
### ✓ Step 2: Integrate Download JSON button and handler into the Galaxy Map page
Add the Download JSON button to the Galaxy Map page and wire up the export trigger.
- Add the "Download JSON" button in the top action bar of `ZeroSpace.Web/Pages/Galaxy.razor` next to the map legend/stats controls.
- Apply consistent Sci-Fi design system styling (`btn-scifi-outline` with appropriate icon/label) in `Galaxy.razor` and `Galaxy.razor.css`.
- Inject `IJSRuntime` in `Galaxy.razor` and implement the `DownloadGalaxyJsonAsync` event handler to generate the export payload and trigger the client-side download named `galaxy-map-data.json`.
- Build the solution and verify clean compilation without errors or warnings.
@@ -0,0 +1,19 @@
namespace ZeroSpace.Model;
public sealed record GalaxyMapExportData(
DateTimeOffset ExportedAt,
int TotalSystems,
int TotalMasteries,
IReadOnlyList<StarSystemData> Systems,
IReadOnlyList<SystemMasteryData> Masteries,
IReadOnlyList<PlanetaryMasteryItemData> PlanetaryMasteryItems)
{
public static GalaxyMapExportData CreateDefault() =>
new(
DateTimeOffset.UtcNow,
StarSystems.All.Count,
SystemMasteries.All.Count,
StarSystems.All,
SystemMasteries.All,
ZeroSpace.Model.PlanetaryMasteryItems.All);
}
+57 -12
View File
@@ -2,6 +2,8 @@
@using System.Globalization
@using System.Text.RegularExpressions
@inject NavigationManager Navigation
@inject IJSRuntime Js
@inject IGalaxyMapExportSerializer ExportSerializer
<PageTitle>Galaxy Map</PageTitle>
@@ -9,7 +11,8 @@
<div class="d-flex flex-wrap align-items-center justify-content-between mb-3 gap-3">
<div>
<h1 class="h2 mb-1 text-light fw-bold">Galaxy Map</h1>
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and hyperlane routes.</p>
<p class="text-secondary mb-0 small">Interactive tactical star chart displaying all planetary systems and
hyperlane routes.</p>
</div>
<div class="d-flex flex-wrap align-items-center gap-3">
<div class="map-legend d-flex flex-wrap align-items-center gap-3 px-3 py-2 rounded">
@@ -34,6 +37,19 @@
<span class="mx-1">•</span>
<span><strong>@_connections.Count</strong> Hyperlanes</span>
</div>
<button class="btn btn-sm btn-scifi-outline d-inline-flex align-items-center gap-2 px-3 py-2"
@onclick="DownloadGalaxyJsonAsync"
disabled="@_isExporting"
title="Download galaxy map data as JSON">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"
aria-hidden="true">
<path
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
</svg>
<span>@(_isExporting ? "Exporting..." : "Download JSON")</span>
</button>
</div>
</div>
@@ -41,7 +57,8 @@
<div class="filter-bar zs-glass-card p-3 mb-3 d-flex flex-wrap align-items-center gap-3">
<div class="d-flex align-items-center gap-2 filter-item">
<label class="filter-label text-dim small fw-semibold text-uppercase">System Type:</label>
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory" @bind:after="ApplyFilters">
<select class="form-select form-select-sm scifi-select" @bind="_selectedCategory"
@bind:after="ApplyFilters">
<option value="">All Types</option>
<option value="@StarSystemCategory.HomeworldSystem">Homeworld System</option>
<option value="@StarSystemCategory.CoreSystem">Core System</option>
@@ -69,7 +86,7 @@
<option value="Influence">Higher Influence</option>
<option value="Credits">Higher Credits</option>
<option value="Energy">Higher Energy</option>
<option value="Items">Higher Items</option>
<option value="Items">Higher Items (In current item, items only attainable in shops)</option>
</select>
</div>
@@ -190,7 +207,8 @@
}
<!-- System Name Label -->
<text y="@SvgNumber(radius + 10)" class="node-label" text-anchor="middle">@system.Name</text>
<text y="@SvgNumber(radius + 10)" class="node-label"
text-anchor="middle">@system.Name</text>
</g>
}
</g>
@@ -210,7 +228,8 @@
<span class="system-owner-indicator"></span>
<span class="system-title fw-bold text-light">@_hoveredSystem.Name</span>
</div>
<span class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
<span
class="badge @CategoryBadgeClass(_hoveredSystem.SystemCategory)">@_hoveredSystem.SystemCategory.ToDisplayName()</span>
</div>
<div class="card-details-grid">
@@ -221,14 +240,16 @@
<div class="detail-row">
<span class="detail-label">Occupation Buff</span>
<span class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
<span
class="detail-value @(_hoveredSystem.OccupationBuff != null ? "text-buff" : "text-secondary")">
@(_hoveredSystem.OccupationBuff ?? "—")
</span>
</div>
<div class="detail-row">
<span class="detail-label">System Mastery</span>
<span class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
<span
class="detail-value @(_hoveredSystem.SystemMastery != null ? "text-mastery" : "text-secondary")">
@(_hoveredSystem.SystemMastery ?? "—")
</span>
</div>
@@ -253,7 +274,8 @@
<div class="detail-row">
<div class="d-flex align-items-center justify-content-between">
<span class="detail-label">Connected Systems</span>
<span class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
<span
class="badge bg-dark text-secondary border border-secondary border-opacity-25">@_hoveredSystem.ConnectedSystems.Count</span>
</div>
<div class="connected-systems-list text-secondary small mt-1">
@string.Join(" • ", _hoveredSystem.ConnectedSystems)
@@ -268,7 +290,8 @@
}
</div>
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")" aria-label="Faction occupational bonuses">
<aside class="faction-drawer zs-glass-card @(_drawerCollapsed ? "is-collapsed" : "")"
aria-label="Faction occupational bonuses">
<div class="drawer-header d-flex align-items-center justify-content-between gap-2">
<div>
<h2 class="drawer-title text-light fw-bold mb-0">Faction Bonuses</h2>
@@ -306,7 +329,8 @@
</p>
@if (FactionBonuses.Count == 0)
{
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's systems.</p>
<p class="text-secondary small mb-0">No occupation buffs recorded for this faction's
systems.</p>
}
else
{
@@ -316,7 +340,8 @@
<li class="bonus-row">
<span class="bonus-total">@FormatBonusTotal(bonus)</span>
<span class="bonus-label">@bonus.Label</span>
<span class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
<span
class="bonus-count">@bonus.PlanetCount @(bonus.PlanetCount == 1 ? "planet" : "planets")</span>
</li>
}
</ul>
@@ -329,7 +354,8 @@
@foreach (var system in FactionSystems)
{
<li>
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)" title="View @system.Name details">
<button class="planet-row" @onclick="() => OnNodeClick(system.Id)"
title="View @system.Name details">
<span class="planet-name">@system.Name</span>
<span class="planet-buff">@(system.OccupationBuff ?? "—")</span>
</button>
@@ -362,6 +388,7 @@
// Faction picker: one of the three factions is always selected.
private string _selectedFaction = StarSystemOwners.EdenAccord;
private bool _drawerCollapsed;
private bool _isExporting;
private bool HasActiveFilters =>
_selectedCategory.HasValue ||
@@ -386,6 +413,7 @@
_matchingSystemIds.Add(system.Id);
}
}
_matchingCount = _matchingSystemIds.Count;
}
@@ -434,6 +462,23 @@
ApplyFilters();
}
private async Task DownloadGalaxyJsonAsync()
{
if (_isExporting) return;
try
{
_isExporting = true;
var exportData = GalaxyMapExportData.CreateDefault();
var json = ExportSerializer.Serialize(exportData);
await Js.InvokeVoidAsync("downloadFileFromText", "galaxy-map-data.json", "application/json", json);
}
finally
{
_isExporting = false;
}
}
// ----- faction bonus drawer -----
public sealed record FactionBonus(string Label, bool IsPercent, double Total, int PlanetCount);
+2
View File
@@ -24,4 +24,6 @@ builder.Services.AddHttpClient<IUnitsApiClient, UnitsApiClient>(client =>
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddSingleton<IGalaxyMapExportSerializer, GalaxyMapExportSerializer>();
await builder.Build().RunAsync();
@@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ZeroSpace.Model;
namespace ZeroSpace.Web.Services;
public sealed class GalaxyMapExportSerializer : IGalaxyMapExportSerializer
{
public static readonly JsonSerializerOptions DefaultOptions = CreateDefaultOptions();
public static JsonSerializerOptions CreateDefaultOptions()
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
return options;
}
public string Serialize(GalaxyMapExportData data) => SerializeData(data);
public static string SerializeData(GalaxyMapExportData data, JsonSerializerOptions? options = null)
{
return JsonSerializer.Serialize(data, options ?? DefaultOptions);
}
}
@@ -0,0 +1,8 @@
using ZeroSpace.Model;
namespace ZeroSpace.Web.Services;
public interface IGalaxyMapExportSerializer
{
string Serialize(GalaxyMapExportData data);
}
@@ -29,6 +29,8 @@
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="js/fileDownload.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
@@ -0,0 +1,11 @@
window.downloadFileFromText = function (fileName, contentType, textContent) {
const blob = new Blob([textContent], { type: contentType });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = fileName || 'download.json';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(url);
};