diff --git a/Petabit.Tests/StarlinkTests.cs b/Petabit.Tests/StarlinkTests.cs index 9a3c5fd..d93b4e2 100644 --- a/Petabit.Tests/StarlinkTests.cs +++ b/Petabit.Tests/StarlinkTests.cs @@ -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); } @@ -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() { diff --git a/Petabit.Tests/iss-video-preview.test.cjs b/Petabit.Tests/iss-video-preview.test.cjs index 0c40df8..b9a358e 100644 --- a/Petabit.Tests/iss-video-preview.test.cjs +++ b/Petabit.Tests/iss-video-preview.test.cjs @@ -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); diff --git a/Petabit.Tests/orbital-clock.test.mjs b/Petabit.Tests/orbital-clock.test.mjs new file mode 100644 index 0000000..e7f7e7a --- /dev/null +++ b/Petabit.Tests/orbital-clock.test.mjs @@ -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); +}); diff --git a/Petabit.Tests/starlink-orbits.test.mjs b/Petabit.Tests/starlink-orbits.test.mjs index 9802b12..855e181 100644 --- a/Petabit.Tests/starlink-orbits.test.mjs +++ b/Petabit.Tests/starlink-orbits.test.mjs @@ -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]); +}); diff --git a/Petabit.Tests/station-dashboard.test.cjs b/Petabit.Tests/station-dashboard.test.cjs new file mode 100644 index 0000000..2365b06 --- /dev/null +++ b/Petabit.Tests/station-dashboard.test.cjs @@ -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); +}); diff --git a/Petabit/Controllers/HomeController.cs b/Petabit/Controllers/HomeController.cs index 656a50c..9d2c90a 100644 --- a/Petabit/Controllers/HomeController.cs +++ b/Petabit/Controllers/HomeController.cs @@ -118,7 +118,8 @@ public async Task 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), diff --git a/Petabit/Resources/Views.Home.Apps.de.resx b/Petabit/Resources/Views.Home.Apps.de.resx index 080d4b4..6561e94 100644 --- a/Petabit/Resources/Views.Home.Apps.de.resx +++ b/Petabit/Resources/Views.Home.Apps.de.resx @@ -120,4 +120,7 @@ Mit Apps lernen + ← Startseite + AUSGEWÄHLTE RESSOURCEN + Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Apps.en.resx b/Petabit/Resources/Views.Home.Apps.en.resx index 5e0edbe..e5f6802 100644 --- a/Petabit/Resources/Views.Home.Apps.en.resx +++ b/Petabit/Resources/Views.Home.Apps.en.resx @@ -120,4 +120,7 @@ Learn with Apps + ← Home + SELECTED RESOURCES + Explore selected resources for learning and developing digital skills. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Apps.hr.resx b/Petabit/Resources/Views.Home.Apps.hr.resx index 01f614c..dad58b0 100644 --- a/Petabit/Resources/Views.Home.Apps.hr.resx +++ b/Petabit/Resources/Views.Home.Apps.hr.resx @@ -120,4 +120,7 @@ Uči pomoću aplikacija + ← Početna + ODABRANI RESURSI + Istraži odabrane resurse za učenje i razvoj digitalnih vještina. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Blockchain.de.resx b/Petabit/Resources/Views.Home.Blockchain.de.resx index 2c3d188..50204e4 100644 --- a/Petabit/Resources/Views.Home.Blockchain.de.resx +++ b/Petabit/Resources/Views.Home.Blockchain.de.resx @@ -123,4 +123,8 @@ Besuche Pi Network - \ No newline at end of file + ← Startseite + AUSGEWÄHLTE RESSOURCEN + Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten. + Blockchain-Technologie + diff --git a/Petabit/Resources/Views.Home.Blockchain.en.resx b/Petabit/Resources/Views.Home.Blockchain.en.resx index ad61398..3d457ab 100644 --- a/Petabit/Resources/Views.Home.Blockchain.en.resx +++ b/Petabit/Resources/Views.Home.Blockchain.en.resx @@ -123,4 +123,8 @@ Visit Pi Network - \ No newline at end of file + ← Home + SELECTED RESOURCES + Explore selected resources for learning and developing digital skills. + Blockchain technology + diff --git a/Petabit/Resources/Views.Home.Blockchain.hr.resx b/Petabit/Resources/Views.Home.Blockchain.hr.resx index 637f07d..cc23a90 100644 --- a/Petabit/Resources/Views.Home.Blockchain.hr.resx +++ b/Petabit/Resources/Views.Home.Blockchain.hr.resx @@ -123,4 +123,8 @@ Posjeti Pi Network - \ No newline at end of file + ← Početna + ODABRANI RESURSI + Istraži odabrane resurse za učenje i razvoj digitalnih vještina. + Blockchain tehnologija + diff --git a/Petabit/Resources/Views.Home.Books.de.resx b/Petabit/Resources/Views.Home.Books.de.resx index 6df79a3..c4ad905 100644 --- a/Petabit/Resources/Views.Home.Books.de.resx +++ b/Petabit/Resources/Views.Home.Books.de.resx @@ -120,4 +120,7 @@ Lerne mit Büchern + ← Startseite + AUSGEWÄHLTE RESSOURCEN + Entdecke ausgewählte Ressourcen zum Lernen und Entwickeln digitaler Fähigkeiten. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Books.en.resx b/Petabit/Resources/Views.Home.Books.en.resx index 353cf13..4d43ad8 100644 --- a/Petabit/Resources/Views.Home.Books.en.resx +++ b/Petabit/Resources/Views.Home.Books.en.resx @@ -120,4 +120,7 @@ Learn with Books + ← Home + SELECTED RESOURCES + Explore selected resources for learning and developing digital skills. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Books.hr.resx b/Petabit/Resources/Views.Home.Books.hr.resx index 6829e27..7d93f9f 100644 --- a/Petabit/Resources/Views.Home.Books.hr.resx +++ b/Petabit/Resources/Views.Home.Books.hr.resx @@ -120,4 +120,7 @@ Uči uz knjige + ← Početna + ODABRANI RESURSI + Istraži odabrane resurse za učenje i razvoj digitalnih vještina. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Index.de.resx b/Petabit/Resources/Views.Home.Index.de.resx index 0ca71f6..32d9647 100644 --- a/Petabit/Resources/Views.Home.Index.de.resx +++ b/Petabit/Resources/Views.Home.Index.de.resx @@ -313,12 +313,23 @@ Satellitenbewegung - Globus mit Positionen betriebsbereiter Starlink-Satelliten - - - 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. + Globus mit Starlink-Satellitenpositionen nach Katalogstatus + 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. Status und Datenquelle + ISS- und Starlink-Tracker + Panel schließen + Nicht betriebsbereit (−) + Reserve / andere Status + Anzeigegeschwindigkeit + Echtzeit · 1× + Simulation · 30× + Satelliten mit Bahndaten + Weitere Status werden nach der nächsten Katalogaktualisierung angezeigt. + Wähle einen Tracker, um die Bahnübersicht zu öffnen. + Wissen für besseren Code. + Lernen durch tägliche Praxis. + Dezentrale Systeme entdecken. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Index.en.resx b/Petabit/Resources/Views.Home.Index.en.resx index 1bbfc51..af3353c 100644 --- a/Petabit/Resources/Views.Home.Index.en.resx +++ b/Petabit/Resources/Views.Home.Index.en.resx @@ -313,12 +313,23 @@ Satellite motion - Globe showing operational Starlink satellite positions - - - 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. + Globe showing Starlink satellite positions by catalog status + 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. Status and data source + ISS and Starlink Tracker + Close panel + Nonoperational (−) + Standby / other statuses + Display speed + Real time · 1× + Simulation · 30× + Satellites with orbital data + Other statuses will be available after the next catalog refresh. + Choose a tracker to open its orbital overview. + Knowledge for better code. + Learn through daily practice. + Explore decentralized systems. \ No newline at end of file diff --git a/Petabit/Resources/Views.Home.Index.hr.resx b/Petabit/Resources/Views.Home.Index.hr.resx index c785aa7..8a1f7a4 100644 --- a/Petabit/Resources/Views.Home.Index.hr.resx +++ b/Petabit/Resources/Views.Home.Index.hr.resx @@ -313,12 +313,23 @@ Kretanje satelita - Globus s položajima operativnih Starlink satelita - - - Plave točke su izračunati položaji operativnih satelita, prema CelesTrak orbitalnim podacima. To nije izravna telemetrija. Povuci globus za promjenu pogleda. Operativni broj prati oznaku + u katalogu, a ne jamstvo pružanja internetske usluge. + Globus s položajima Starlink satelita prema statusu u katalogu + Boje prate status u CelesTrak katalogu. Položaji su izračunati iz dostupnih orbitalnih podataka, a nisu izravna telemetrija. Povuci globus za promjenu pogleda. Ubrzani prikaz označen je kao simulacija. Status i izvor podataka + ISS i Starlink Tracker + Zatvori panel + Neoperativni (−) + Rezervni / drugi statusi + Brzina prikaza + Stvarno vrijeme · 1× + Simulacija · 30× + Sateliti s orbitalnim podacima + Prikaz drugih statusa bit će dostupan nakon sljedećeg osvježavanja kataloga. + Odaberi tracker i otvori njegov orbitalni pregled. + Znanje za kvalitetniji kod. + Učenje kroz svakodnevnu praksu. + Istraži decentralizirane sustave. \ No newline at end of file diff --git a/Petabit/Services/StarlinkService.cs b/Petabit/Services/StarlinkService.cs index 6b77bb9..bb310fa 100644 --- a/Petabit/Services/StarlinkService.cs +++ b/Petabit/Services/StarlinkService.cs @@ -4,7 +4,8 @@ namespace Petabit.Services; public sealed record StarlinkSnapshot(int OperationalCount, int OnOrbitCount, int PartiallyOperationalCount, - DateTimeOffset RetrievedAt, DateTimeOffset OldestEpoch, DateTimeOffset NewestEpoch, JsonElement[] Elements); + DateTimeOffset RetrievedAt, DateTimeOffset OldestEpoch, DateTimeOffset NewestEpoch, JsonElement[] Elements, + int NonOperationalCount = 0, int OtherCount = 0, bool StatusCoverageComplete = false); public sealed class StarlinkService { @@ -133,7 +134,7 @@ public static StarlinkSnapshot Parse(string catalogJson, string elementsJson, Da if (entry.ValueKind != JsonValueKind.Object) throw new FormatException("Invalid orbital entry."); if (!entry.TryGetProperty("NORAD_CAT_ID", out var id) || id.ValueKind != JsonValueKind.Number || !id.TryGetInt32(out var number)) throw new FormatException("Invalid orbital ID."); - if (!operational.Contains(number) || !seen.Add(number)) continue; + if (!onOrbit.ContainsKey(number) || !seen.Add(number)) continue; if (!Text(entry, "OBJECT_NAME").StartsWith("STARLINK-", StringComparison.Ordinal) || !DateTimeOffset.TryParse(Text(entry, "EPOCH"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var epoch) @@ -146,12 +147,16 @@ public static StarlinkSnapshot Parse(string catalogJson, string elementsJson, Da || entry.GetProperty("ECCENTRICITY").GetDouble() is < 0 or >= 1 || entry.GetProperty("INCLINATION").GetDouble() is < 0 or > 180) throw new FormatException("Invalid orbital range."); - orbits.Add(entry.Clone()); + var tagged = new Dictionary(); + foreach (var property in entry.EnumerateObject()) tagged[property.Name] = property.Value; + tagged["PETABIT_STATUS"] = JsonSerializer.SerializeToElement(onOrbit[number]); + orbits.Add(JsonSerializer.SerializeToElement(tagged)); epochs.Add(epoch); } - if (orbits.Count == 0) throw new FormatException("No operational Starlink orbital elements."); + if (orbits.Count == 0) throw new FormatException("No valid Starlink orbital elements."); return new(operational.Count, onOrbit.Count, onOrbit.Count(pair => pair.Value == "P"), retrievedAt, - epochs.Min(), epochs.Max(), orbits.ToArray()); + epochs.Min(), epochs.Max(), orbits.ToArray(), onOrbit.Count(pair => pair.Value == "-"), + onOrbit.Count(pair => pair.Value is not ("+" or "P" or "-")), true); } private static string Text(JsonElement entry, string name) @@ -160,21 +165,28 @@ private static string Text(JsonElement entry, string name) private static bool ValidCache(StarlinkSnapshot saved) { if (saved.Elements is not { Length: > 0 and <= 50_000 } - || saved.OnOrbitCount is <= 0 or > 50_000 || saved.OperationalCount < saved.Elements.Length + || saved.OnOrbitCount is <= 0 or > 50_000 || saved.OnOrbitCount < saved.Elements.Length || saved.OperationalCount < 0 || saved.OperationalCount > saved.OnOrbitCount || saved.PartiallyOperationalCount < 0 || saved.PartiallyOperationalCount > saved.OnOrbitCount - saved.OperationalCount + || saved.NonOperationalCount < 0 || saved.OtherCount < 0 + || saved.NonOperationalCount > saved.OnOrbitCount || saved.OtherCount > saved.OnOrbitCount + || (saved.StatusCoverageComplete && saved.OperationalCount + saved.PartiallyOperationalCount + saved.NonOperationalCount + saved.OtherCount != saved.OnOrbitCount) || saved.RetrievedAt > DateTimeOffset.UtcNow) return false; // Apply the same schema validation to cached elements as to an upstream response. var catalog = saved.Elements.Select(entry => new { OBJECT_NAME = Text(entry, "OBJECT_NAME"), NORAD_CAT_ID = entry.GetProperty("NORAD_CAT_ID"), OBJECT_TYPE = "PAY", - OPS_STATUS_CODE = "+", ORBIT_CENTER = "EA", ORBIT_TYPE = "ORB", DECAY_DATE = "" + OPS_STATUS_CODE = saved.StatusCoverageComplete ? Text(entry, "PETABIT_STATUS") : "+", + ORBIT_CENTER = "EA", ORBIT_TYPE = "ORB", DECAY_DATE = "" }); try { var validated = Parse(JsonSerializer.Serialize(catalog), JsonSerializer.Serialize(saved.Elements), saved.RetrievedAt); - return validated.Elements.Length == saved.Elements.Length && validated.OldestEpoch == saved.OldestEpoch + return validated.Elements.Length == saved.Elements.Length && validated.OperationalCount <= saved.OperationalCount + && (!saved.StatusCoverageComplete || (validated.NonOperationalCount <= saved.NonOperationalCount + && validated.PartiallyOperationalCount <= saved.PartiallyOperationalCount && validated.OtherCount <= saved.OtherCount)) + && validated.OldestEpoch == saved.OldestEpoch && validated.NewestEpoch == saved.NewestEpoch; } catch (Exception error) when (error is FormatException or JsonException or InvalidOperationException or KeyNotFoundException) diff --git a/Petabit/Views/Home/Apps.cshtml b/Petabit/Views/Home/Apps.cshtml index b67d484..195e77a 100644 --- a/Petabit/Views/Home/Apps.cshtml +++ b/Petabit/Views/Home/Apps.cshtml @@ -6,9 +6,12 @@ @inject IViewLocalizer Localizer -
+
+ @Localizer["BackHome"] +

@Localizer["ExploreResources"]

📱 @Localizer["LearnWithApps"]

-
    +

    @Localizer["PageIntro"]

    +
    • Mimo App
    • diff --git a/Petabit/Views/Home/Blockchain.cshtml b/Petabit/Views/Home/Blockchain.cshtml index 8ae2b19..d3ab119 100644 --- a/Petabit/Views/Home/Blockchain.cshtml +++ b/Petabit/Views/Home/Blockchain.cshtml @@ -2,12 +2,15 @@ @inject IViewLocalizer Localizer @{ - ViewData["Title"] = Localizer["BlockchainTechnology"]; + ViewData["Title"] = Localizer["BlockchainTechnology"].Value; ViewData["Description"] = "Uvod u blockchain tehnologiju i korisni resursi za daljnje istraživanje decentraliziranih sustava."; } -
      -

      🪙 @Localizer["BlockchainTechnology"]

      +
      + @Localizer["BackHome"] +

      @Localizer["ExploreResources"]

      +

      🪙 @Localizer["BlockchainTechnology"]

      +

      @Localizer["PageIntro"]

      diff --git a/Petabit/Views/Home/Books.cshtml b/Petabit/Views/Home/Books.cshtml index 30cc9ce..405f187 100644 --- a/Petabit/Views/Home/Books.cshtml +++ b/Petabit/Views/Home/Books.cshtml @@ -6,9 +6,12 @@ @using Microsoft.AspNetCore.Mvc.Localization @inject IViewLocalizer Localizer -

      +
      + @Localizer["BackHome"] +

      @Localizer["ExploreResources"]

      📚 @Localizer["LearnWithBooks"]

      -
        +

        @Localizer["PageIntro"]

        +
        • @Localizer["Clean Code – Robert C. Martin"]
        • diff --git a/Petabit/Views/Home/Index.cshtml b/Petabit/Views/Home/Index.cshtml index f181dbd..ef74dc2 100644 --- a/Petabit/Views/Home/Index.cshtml +++ b/Petabit/Views/Home/Index.cshtml @@ -4,7 +4,7 @@ @{ // Postavlja naslov stranice koji će se prikazati u pregledniku - ViewData["Title"] = "Petabit – učenje, tehnologija i ISS Tracker"; + ViewData["Title"] = "Petabit – ISS i Starlink Tracker"; ViewData["Description"] = "Istraži Petabit: višejezičnu platformu za učenje, tehnološke preporuke, blockchain sadržaj i praćenje Međunarodne svemirske postaje uživo."; var cspNonce = Context.Items["CspNonce"]?.ToString(); } @@ -55,24 +55,24 @@
          -
          + -

          🌐 ISS Tracker

          +

          🌐 @Localizer["TrackerTitle"]

          @Localizer["TrackerIntro"]

          - - +
          +
          @@ -80,49 +80,51 @@
          -
          -
          -
          -

          @Localizer["HologramKicker"]

          -

          @Localizer["HologramTitle"]

          -
          - @Localizer["HologramWaiting"] -
          -
          - - -
          -

          @Localizer["HologramNote"]

          -
          -
          - + CelesTrak · @Localizer["StarlinkSource"] +
      @@ -321,15 +324,18 @@ } } - if (!reduceMotion.matches) globeAnimationFrame = requestAnimationFrame(drawGlobe); + if (!reduceMotion.matches && !document.hidden && !document.getElementById('iss-panel').hidden) globeAnimationFrame = requestAnimationFrame(drawGlobe); } function restartGlobe() { cancelAnimationFrame(globeAnimationFrame); + if (document.getElementById('iss-panel').hidden || document.hidden) return; resizeGlobeCanvas(); drawGlobe(); } + window.addEventListener('iss-panel-open', restartGlobe); + window.addEventListener('iss-panel-close', () => cancelAnimationFrame(globeAnimationFrame)); restartGlobe(); window.addEventListener('resize', restartGlobe, { passive: true }); reduceMotion.addEventListener('change', restartGlobe); diff --git a/Petabit/Views/Shared/_Layout.cshtml b/Petabit/Views/Shared/_Layout.cshtml index 1dfb646..48896b4 100644 --- a/Petabit/Views/Shared/_Layout.cshtml +++ b/Petabit/Views/Shared/_Layout.cshtml @@ -1,4 +1,4 @@ -@**@ +@**@ @using Microsoft.AspNetCore.Localization @using System.Text.Encodings.Web @using System.Text.Json @@ -81,6 +81,7 @@ + @RenderSection("Styles", required: false) + +