Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions Petabit.Tests/StarlinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ public void OperationalIsStrictlyPlusNotAllActiveAndDecayedObjectsAreExcluded()
{
var catalog = JsonSerializer.Serialize(new[] { Catalog(100800, "+"), Catalog(2, "P"), Catalog(3, "S"),
Catalog(4, "-"), Catalog(5, "D", "2026-10-01"), Catalog(6, "+", type: "IMP"), Catalog(7, "+") });
var state = StarlinkService.Parse(catalog, JsonSerializer.Serialize(new[] { Orbit(100800), Orbit(2), Orbit(100800) }), Now);
var state = StarlinkService.Parse(catalog, JsonSerializer.Serialize(new[] { Orbit(100800), Orbit(2), Orbit(3), Orbit(4), Orbit(100800) }), Now);
Assert.Equal(2, state.OperationalCount);
Assert.Equal(5, state.OnOrbitCount);
Assert.Equal(1, state.PartiallyOperationalCount);
Assert.Single(state.Elements);
Assert.Equal(4, state.Elements.Length);
Assert.Equal(1, state.NonOperationalCount);
Assert.Equal(1, state.OtherCount);
Assert.True(state.StatusCoverageComplete);
Assert.Equal(new[] { "+", "P", "S", "-" }, state.Elements.Select(e => e.GetProperty("PETABIT_STATUS").GetString()));
Assert.Equal(100800, state.Elements[0].GetProperty("NORAD_CAT_ID").GetInt32());
Assert.Equal(DateTimeOffset.Parse("2026-10-04T22:36:12.73824Z"), state.OldestEpoch);
}
Expand Down Expand Up @@ -93,6 +97,37 @@ public async Task CorruptPersistedElementsAreDiscardedAndReplacedFromSource()
Assert.Equal(2, handler.Requests);
}

[Fact]
public async Task MixedStatusSnapshotSurvivesRestartWithoutDownloadingAgain()
{
Directory.CreateDirectory(Path.Combine(directory, "App_Data"));
var snapshot = StarlinkService.Parse(JsonSerializer.Serialize(new[] { Catalog(1, "+"), Catalog(2, "-"), Catalog(3, "P"), Catalog(4, "S") }),
JsonSerializer.Serialize(new[] { Orbit(1), Orbit(2), Orbit(3), Orbit(4) }), DateTimeOffset.UtcNow);
await File.WriteAllTextAsync(Path.Combine(directory, "App_Data", "starlink-status.json"), JsonSerializer.Serialize(snapshot));
var handler = new Handler();
var restored = await Create(handler).GetAsync(CancellationToken.None);
Assert.True(restored.StatusCoverageComplete);
Assert.Equal(1, restored.NonOperationalCount);
Assert.Equal(1, restored.OtherCount);
Assert.Equal(new[] { "+", "-", "P", "S" }, restored.Elements.Select(e => e.GetProperty("PETABIT_STATUS").GetString()));
Assert.Equal(0, handler.Requests);
}

[Fact]
public async Task LegacyOperationalOnlyCachePreservesUnknownStatusCoverageUntilRefreshDue()
{
Directory.CreateDirectory(Path.Combine(directory, "App_Data"));
var snapshot = new { OperationalCount = 2, OnOrbitCount = 3, PartiallyOperationalCount = 1,
RetrievedAt = DateTimeOffset.UtcNow, OldestEpoch = DateTimeOffset.Parse("2026-10-04T22:36:12.73824Z"),
NewestEpoch = DateTimeOffset.Parse("2026-10-04T22:36:12.73824Z"), Elements = new[] { Orbit(1) } };
await File.WriteAllTextAsync(Path.Combine(directory, "App_Data", "starlink-status.json"), JsonSerializer.Serialize(snapshot));
var handler = new Handler();
var restored = await Create(handler).GetAsync(CancellationToken.None);
Assert.False(restored.StatusCoverageComplete);
Assert.Equal(2, restored.OperationalCount);
Assert.Equal(0, handler.Requests);
}

[Fact]
public async Task RepeatedPingAndRestartReuseThePersistedTwoHourCache()
{
Expand Down
13 changes: 13 additions & 0 deletions Petabit.Tests/iss-video-preview.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ function harness() {
};
}

test('closing ISS destroys playback and keeps late player callbacks from reopening it', async () => {
const h = harness();
await h.start();
h.players[0].state(1);
h.events.get('iss-panel-close')();
assert.equal(h.elements.get('iss-video-panel').hidden,true);
assert.equal(h.players[0].destroyed,true);
h.players[0].state(1);
h.tick(10000);
assert.equal(h.elements.get('iss-video-panel').hidden,true);
assert.equal(h.elements.get('iss-video-panel').dataset.playback,'closed');
});

test('does not contact YouTube/NASA or show the panel before manual Ping', () => {
const app = harness();
assert.equal(app.requests(), 0);
Expand Down
26 changes: 26 additions & 0 deletions Petabit.Tests/orbital-clock.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { OrbitalClock } from '../Petabit/wwwroot/js/orbital-clock.mjs';
test('30× simulation advances orbital time and pauses without jumping on resume', () => {
const clock = new OrbitalClock(1000);
clock.configure(true,30,1000);
assert.equal(clock.now(2000),31000);
clock.configure(false,30,2000);
assert.equal(clock.now(6000),31000);
clock.configure(true,30,6000);
assert.equal(clock.now(7000),61000);
});
test('switching from simulation to real time returns to current wall time', () => {
const clock = new OrbitalClock(1000);
clock.configure(true,30,1000);
clock.configure(true,1,5000);
assert.equal(clock.now(6000),6000);
});
test('real-time resume catches up to wall time after a hidden tab', () => {
const clock = new OrbitalClock(1000);
clock.configure(true, 1, 1000);
clock.configure(false, 1, 2000);
assert.equal(clock.now(6000), 2000);
clock.configure(true, 1, 6000);
assert.equal(clock.now(7000), 7000);
});
10 changes: 10 additions & 0 deletions Petabit.Tests/starlink-orbits.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ test('unpropagatable elements are omitted rather than displaying invented locati
const records = createRecords([{ ...omm, ECCENTRICITY: 2 }]);
assert.equal(positionsAt(records, Date.parse(omm.EPOCH + 'Z')).count, 0);
});
test('status colors stay aligned when invalid orbits are omitted', () => {
const records = createRecords([
{ ...omm, PETABIT_STATUS: '+', ECCENTRICITY: 2 },
{ ...omm, PETABIT_STATUS: '-' }, { ...omm, PETABIT_STATUS: 'P' },
{ ...omm, PETABIT_STATUS: '?' }
]);
const frame = positionsAt(records, Date.parse(omm.EPOCH + 'Z'));
assert.equal(frame.count,3);
assert.deepEqual([...frame.statuses],[2,1,3]);
});
44 changes: 44 additions & 0 deletions Petabit.Tests/station-dashboard.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
function harness() {
const events = new Map(), listeners = new Map(), requests = [];
const elements = new Map();
for (const id of ['station-dashboard','iss-led-localization','iss-ping-button','iss-panel','iss-close','iss-led-display','iss-led-message','iss-sound-enabled']) {
elements.set(id, { hidden: true, disabled: false, checked: false, dataset: { loading: 'loading', refresh: 'Ping ISS' },
textContent: '', addEventListener: (name, fn) => listeners.set(`${id}:${name}`, fn),
setAttribute() {}, scrollIntoView() {}, focus() { this.focused = true; }, classList: { add() {} } });
}
const document = { hidden: false, getElementById: id => elements.get(id),
addEventListener: (name, fn) => events.set(name, fn), querySelector: () => ({ prepend() {} }) };
const window = { dispatchEvent() {}, setInterval: fn => { events.set('interval', fn); } };
vm.runInNewContext(fs.readFileSync(require('node:path').join(__dirname,'../Petabit/wwwroot/js/station-dashboard.js'),'utf8'), {
document, window, Event, AbortController, AbortSignal, fetch: (url, options) => new Promise(resolve => requests.push({ url, signal: options.signal, resolve }))
});
events.get('DOMContentLoaded')();
return { elements, events, requests, click: id => listeners.get(`${id}:click`)() };
}
test('hidden ISS panel performs no initial, interval or visibility data requests', () => {
const h = harness();
h.events.get('interval')(); h.events.get('visibilitychange')();
assert.equal(h.requests.length, 0);
h.click('iss-ping-button');
assert.equal(h.requests.length, 2);
assert.equal(h.elements.get('iss-panel').hidden, false);
});
test('closing pending ISS cancels visitor requests and allows immediate reopening; late responses are ignored', async () => {
const h = harness();
h.click('iss-ping-button');
h.click('iss-close');
assert.equal(h.elements.get('iss-panel').hidden, true);
assert.equal(h.elements.get('iss-ping-button').disabled, false);
assert.equal(h.elements.get('iss-ping-button').focused, true);
assert.ok(h.requests.every(r => r.signal.aborted));
for (const r of h.requests) r.resolve({ ok: false, status: 503 });
await new Promise(resolve => setImmediate(resolve));
assert.equal(h.elements.get('iss-panel').hidden, true);
h.click('iss-ping-button');
assert.equal(h.requests.length, 4);
assert.equal(h.elements.get('iss-panel').hidden, false);
});
3 changes: 2 additions & 1 deletion Petabit/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public async Task<IActionResult> StarlinkData([FromServices] Petabit.Services.St
var data = await starlink.GetAsync(cancellationToken);
return Json(new
{
data.OperationalCount, data.OnOrbitCount, data.PartiallyOperationalCount, data.RetrievedAt,
data.OperationalCount, data.OnOrbitCount, data.PartiallyOperationalCount, data.NonOperationalCount,
data.OtherCount, data.StatusCoverageComplete, data.RetrievedAt,
data.OldestEpoch, data.NewestEpoch, data.Elements,
isStale = DateTimeOffset.UtcNow - data.RetrievedAt > TimeSpan.FromHours(4)
|| DateTimeOffset.UtcNow - data.OldestEpoch > TimeSpan.FromDays(3.5),
Expand Down
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Apps.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithApps" xml:space="preserve">
<value>Mit Apps lernen</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Startseite</value></data>
<data name="ExploreResources" xml:space="preserve"><value>AUSGEWÄHLTE RESSOURCEN</value></data>
<data name="PageIntro" xml:space="preserve"><value>Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten.</value></data>
</root>
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Apps.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithApps" xml:space="preserve">
<value>Learn with Apps</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Home</value></data>
<data name="ExploreResources" xml:space="preserve"><value>SELECTED RESOURCES</value></data>
<data name="PageIntro" xml:space="preserve"><value>Explore selected resources for learning and developing digital skills.</value></data>
</root>
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Apps.hr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithApps" xml:space="preserve">
<value>Uči pomoću aplikacija</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Početna</value></data>
<data name="ExploreResources" xml:space="preserve"><value>ODABRANI RESURSI</value></data>
<data name="PageIntro" xml:space="preserve"><value>Istraži odabrane resurse za učenje i razvoj digitalnih vještina.</value></data>
</root>
6 changes: 5 additions & 1 deletion Petabit/Resources/Views.Home.Blockchain.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@
<data name="VisitPiNetwork" xml:space="preserve">
<value>Besuche Pi Network</value>
</data>
</root>
<data name="BackHome" xml:space="preserve"><value>← Startseite</value></data>
<data name="ExploreResources" xml:space="preserve"><value>AUSGEWÄHLTE RESSOURCEN</value></data>
<data name="PageIntro" xml:space="preserve"><value>Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten.</value></data>
<data name="BlockchainTechnology" xml:space="preserve"><value>Blockchain-Technologie</value></data>
</root>
6 changes: 5 additions & 1 deletion Petabit/Resources/Views.Home.Blockchain.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@
<data name="VisitPiNetwork" xml:space="preserve">
<value>Visit Pi Network</value>
</data>
</root>
<data name="BackHome" xml:space="preserve"><value>← Home</value></data>
<data name="ExploreResources" xml:space="preserve"><value>SELECTED RESOURCES</value></data>
<data name="PageIntro" xml:space="preserve"><value>Explore selected resources for learning and developing digital skills.</value></data>
<data name="BlockchainTechnology" xml:space="preserve"><value>Blockchain technology</value></data>
</root>
6 changes: 5 additions & 1 deletion Petabit/Resources/Views.Home.Blockchain.hr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@
<data name="VisitPiNetwork" xml:space="preserve">
<value>Posjeti Pi Network</value>
</data>
</root>
<data name="BackHome" xml:space="preserve"><value>← Početna</value></data>
<data name="ExploreResources" xml:space="preserve"><value>ODABRANI RESURSI</value></data>
<data name="PageIntro" xml:space="preserve"><value>Istraži odabrane resurse za učenje i razvoj digitalnih vještina.</value></data>
<data name="BlockchainTechnology" xml:space="preserve"><value>Blockchain tehnologija</value></data>
</root>
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Books.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithBooks" xml:space="preserve">
<value>Lerne mit Büchern</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Startseite</value></data>
<data name="ExploreResources" xml:space="preserve"><value>AUSGEWÄHLTE RESSOURCEN</value></data>
<data name="PageIntro" xml:space="preserve"><value>Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten.</value></data>
</root>
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Books.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithBooks" xml:space="preserve">
<value>Learn with Books</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Home</value></data>
<data name="ExploreResources" xml:space="preserve"><value>SELECTED RESOURCES</value></data>
<data name="PageIntro" xml:space="preserve"><value>Explore selected resources for learning and developing digital skills.</value></data>
</root>
3 changes: 3 additions & 0 deletions Petabit/Resources/Views.Home.Books.hr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,7 @@
<data name="LearnWithBooks" xml:space="preserve">
<value>Uči uz knjige</value>
</data>
<data name="BackHome" xml:space="preserve"><value>← Početna</value></data>
<data name="ExploreResources" xml:space="preserve"><value>ODABRANI RESURSI</value></data>
<data name="PageIntro" xml:space="preserve"><value>Istraži odabrane resurse za učenje i razvoj digitalnih vještina.</value></data>
</root>
19 changes: 15 additions & 4 deletions Petabit/Resources/Views.Home.Index.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,23 @@
<value>Satellitenbewegung</value>
</data>
<data name="StarlinkCanvas" xml:space="preserve">
<value>Globus mit Positionen betriebsbereiter Starlink-Satelliten</value>
</data>
<data name="StarlinkNote" xml:space="preserve">
<value>Blaue Punkte zeigen berechnete Positionen betriebsbereiter Satelliten anhand von CelesTrak-Orbitaldaten. Dies ist keine direkte Telemetrie. Ziehe den Globus, um die Ansicht zu ändern. Die Anzahl folgt der Katalogkennzeichnung + und garantiert keinen Internetdienst.</value>
<value>Globus mit Starlink-Satellitenpositionen nach Katalogstatus</value>
</data>
<data name="StarlinkNote" xml:space="preserve"><value>Farben entsprechen dem Status im CelesTrak-Katalog. Positionen werden aus verfügbaren Bahndaten berechnet und sind keine direkte Telemetrie. Ziehe den Globus, um die Ansicht zu ändern. Beschleunigte Wiedergabe ist als Simulation gekennzeichnet.</value></data>
<data name="StarlinkSource" xml:space="preserve">
<value>Status und Datenquelle</value>
</data>
<data name="TrackerTitle" xml:space="preserve"><value>ISS- und Starlink-Tracker</value></data>
<data name="ClosePanel" xml:space="preserve"><value>Panel schließen</value></data>
<data name="StarlinkNonOperational" xml:space="preserve"><value>Nicht betriebsbereit (−)</value></data>
<data name="StarlinkOther" xml:space="preserve"><value>Reserve / andere Status</value></data>
<data name="StarlinkSpeed" xml:space="preserve"><value>Anzeigegeschwindigkeit</value></data>
<data name="StarlinkRealtime" xml:space="preserve"><value>Echtzeit · 1×</value></data>
<data name="StarlinkSimulation" xml:space="preserve"><value>Simulation · 30×</value></data>
<data name="StarlinkCoverage" xml:space="preserve"><value>Satelliten mit Bahndaten</value></data>
<data name="StarlinkLegacy" xml:space="preserve"><value>Weitere Status werden nach der nächsten Katalogaktualisierung angezeigt.</value></data>
<data name="TrackerIntro" xml:space="preserve"><value>Wähle einen Tracker, um die Bahnübersicht zu öffnen.</value></data>
<data name="BooksSummary" xml:space="preserve"><value>Wissen für besseren Code.</value></data>
<data name="AppsSummary" xml:space="preserve"><value>Lernen durch tägliche Praxis.</value></data>
<data name="BlockchainSummary" xml:space="preserve"><value>Dezentrale Systeme entdecken.</value></data>
</root>
19 changes: 15 additions & 4 deletions Petabit/Resources/Views.Home.Index.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,23 @@
<value>Satellite motion</value>
</data>
<data name="StarlinkCanvas" xml:space="preserve">
<value>Globe showing operational Starlink satellite positions</value>
</data>
<data name="StarlinkNote" xml:space="preserve">
<value>Blue dots show calculated positions of operational satellites from CelesTrak orbital data. This is not direct telemetry. Drag the globe to change the view. The operational count follows the catalog + flag and does not guarantee internet service.</value>
<value>Globe showing Starlink satellite positions by catalog status</value>
</data>
<data name="StarlinkNote" xml:space="preserve"><value>Colors follow the CelesTrak catalog status. Positions are calculated from available orbital data, not direct telemetry. Drag the globe to change your viewpoint. Accelerated playback is labeled as a simulation.</value></data>
<data name="StarlinkSource" xml:space="preserve">
<value>Status and data source</value>
</data>
<data name="TrackerTitle" xml:space="preserve"><value>ISS and Starlink Tracker</value></data>
<data name="ClosePanel" xml:space="preserve"><value>Close panel</value></data>
<data name="StarlinkNonOperational" xml:space="preserve"><value>Nonoperational (−)</value></data>
<data name="StarlinkOther" xml:space="preserve"><value>Standby / other statuses</value></data>
<data name="StarlinkSpeed" xml:space="preserve"><value>Display speed</value></data>
<data name="StarlinkRealtime" xml:space="preserve"><value>Real time · 1×</value></data>
<data name="StarlinkSimulation" xml:space="preserve"><value>Simulation · 30×</value></data>
<data name="StarlinkCoverage" xml:space="preserve"><value>Satellites with orbital data</value></data>
<data name="StarlinkLegacy" xml:space="preserve"><value>Other statuses will be available after the next catalog refresh.</value></data>
<data name="TrackerIntro" xml:space="preserve"><value>Choose a tracker to open its orbital overview.</value></data>
<data name="BooksSummary" xml:space="preserve"><value>Knowledge for better code.</value></data>
<data name="AppsSummary" xml:space="preserve"><value>Learn through daily practice.</value></data>
<data name="BlockchainSummary" xml:space="preserve"><value>Explore decentralized systems.</value></data>
</root>
Loading
Loading