fix
This commit is contained in:
@@ -32,6 +32,16 @@ pub async fn release_run(
|
|||||||
launcher.release_run(password, with_api, with_mcp).await
|
launcher.release_run(password, with_api, with_mcp).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn refresh_run(
|
||||||
|
launcher: State<'_, Launcher>,
|
||||||
|
password: String,
|
||||||
|
pg_password: String,
|
||||||
|
tile_root: String,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
launcher.refresh_run(password, pg_password, tile_root).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn release_console(launcher: State<'_, Launcher>, since: usize) -> CmdResult<crate::release::ConsoleTail> {
|
pub fn release_console(launcher: State<'_, Launcher>, since: usize) -> CmdResult<crate::release::ConsoleTail> {
|
||||||
Ok(launcher.release_console(since))
|
Ok(launcher.release_console(since))
|
||||||
|
|||||||
@@ -2539,6 +2539,32 @@ impl Launcher {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refresh the pack's data payload in its own run. Returns once it has started.
|
||||||
|
pub async fn refresh_run(&self, password: String, pg_password: String, tile_root: String) -> Result<(), String> {
|
||||||
|
if self.release_console.lock().is_ok_and(|c| c.is_running()) {
|
||||||
|
return Err("Something is already running: wait for it to finish.".to_string());
|
||||||
|
}
|
||||||
|
if password.trim().is_empty() {
|
||||||
|
return Err("The node's sudo password is needed: the dump runs there.".to_string());
|
||||||
|
}
|
||||||
|
let (dev_root, install_dir) = {
|
||||||
|
let inner = self.inner.lock().await;
|
||||||
|
(inner.settings.dev_root.trim().to_string(), inner.settings.install_path())
|
||||||
|
};
|
||||||
|
if dev_root.is_empty() {
|
||||||
|
return Err("Dev mode has no checkout folder, so the refresh scripts cannot be found.".to_string());
|
||||||
|
}
|
||||||
|
crate::release::run_refresh(
|
||||||
|
&PathBuf::from(dev_root).join("ODIAN_MCP_Server"),
|
||||||
|
&install_dir.join("state").join("release"),
|
||||||
|
pg_password.trim(),
|
||||||
|
Some(tile_root.as_str()),
|
||||||
|
&password,
|
||||||
|
&self.release_console,
|
||||||
|
)
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
/// Everything the release has printed since line `since`.
|
/// Everything the release has printed since line `since`.
|
||||||
pub fn release_console(&self, since: usize) -> crate::release::ConsoleTail {
|
pub fn release_console(&self, since: usize) -> crate::release::ConsoleTail {
|
||||||
match self.release_console.lock() {
|
match self.release_console.lock() {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ pub fn run() {
|
|||||||
commands::release_plan,
|
commands::release_plan,
|
||||||
commands::release_run,
|
commands::release_run,
|
||||||
commands::release_console,
|
commands::release_console,
|
||||||
|
commands::refresh_run,
|
||||||
commands::get_market,
|
commands::get_market,
|
||||||
commands::apply_market,
|
commands::apply_market,
|
||||||
commands::check_market,
|
commands::check_market,
|
||||||
|
|||||||
+99
-13
@@ -138,9 +138,14 @@ pub struct ReleaseData {
|
|||||||
pub tile_bytes: u64,
|
pub tile_bytes: u64,
|
||||||
/// Every distinct dataset the tile sets name.
|
/// Every distinct dataset the tile sets name.
|
||||||
pub tile_datasets: Vec<String>,
|
pub tile_datasets: Vec<String>,
|
||||||
/// The age of the OLDEST dated dataset, which is what decides how stale the maps are. A
|
/// The basemap's SOURCE vintage, as `YYYYMMDD`, taken from the dataset name
|
||||||
/// name carrying no date (`planet_heightmap`) is not counted: that one does not move.
|
/// (`protomaps_20260423`). This is the date of the upstream extract the maps were drawn
|
||||||
pub tiles_age_days: Option<i64>,
|
/// from - NOT when anyone baked or froze them. Nothing in a tile index or a manifest
|
||||||
|
/// records a bake time, so that age simply does not exist to be shown, and calling this
|
||||||
|
/// one "old" read as if the pack were stale when it only means the world data is from
|
||||||
|
/// April. An undated name (`planet_heightmap`) is skipped: that set does not move.
|
||||||
|
pub basemap_vintage: Option<String>,
|
||||||
|
pub basemap_vintage_days: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Days from 1970-01-01 to y-m-d, proleptic Gregorian. Hinnant's algorithm, which is exact
|
/// Days from 1970-01-01 to y-m-d, proleptic Gregorian. Hinnant's algorithm, which is exact
|
||||||
@@ -201,11 +206,13 @@ pub fn data_from_manifest(m: &serde_json::Value, version: &str, is_live: bool, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tiles_age_days = tile_datasets
|
// The oldest dated extract, since that is the one that limits what the maps show.
|
||||||
|
let basemap_vintage = tile_datasets
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|d| dataset_stamp(d))
|
.filter_map(|d| dataset_stamp(d))
|
||||||
.filter_map(|s| age_days(s, now_secs))
|
.filter_map(|s| age_days(s, now_secs).map(|age| (age, s.to_string())))
|
||||||
.max();
|
.max_by_key(|(age, _)| *age)
|
||||||
|
.map(|(_, s)| s);
|
||||||
ReleaseData {
|
ReleaseData {
|
||||||
version: version.to_string(),
|
version: version.to_string(),
|
||||||
is_live,
|
is_live,
|
||||||
@@ -215,7 +222,8 @@ pub fn data_from_manifest(m: &serde_json::Value, version: &str, is_live: bool, n
|
|||||||
tile_sets: sets.map(|s| s.len()).unwrap_or(0),
|
tile_sets: sets.map(|s| s.len()).unwrap_or(0),
|
||||||
tile_bytes,
|
tile_bytes,
|
||||||
tile_datasets,
|
tile_datasets,
|
||||||
tiles_age_days,
|
basemap_vintage_days: basemap_vintage.as_deref().and_then(|s| age_days(s, now_secs)),
|
||||||
|
basemap_vintage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,18 +449,78 @@ pub fn run_release(
|
|||||||
// No `pause` here any more: this runs into the launcher's own panel, where there is no
|
// No `pause` here any more: this runs into the launcher's own panel, where there is no
|
||||||
// console to press Enter in. The exit code carries the verdict instead - swallowing a
|
// console to press Enter in. The exit code carries the verdict instead - swallowing a
|
||||||
// failure and exiting 0 would make a failed release look like a finished one.
|
// failure and exiting 0 would make a failed release look like a finished one.
|
||||||
let ps = format!(
|
let ps = console_script(&body, &log, "Release finished.");
|
||||||
|
|
||||||
|
spawn_console(work_dir, "release.ps1", &ps, &[("ODIAN_NODE_SUDO", sudo)], console)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the data a pack carries: the database seed, and optionally the tile indexes.
|
||||||
|
///
|
||||||
|
/// Deliberately NOT part of a release, and deliberately its own press. The seed is a
|
||||||
|
/// per-table dump of a database operators are using, and the tiles are indexed where the
|
||||||
|
/// tiles are. Neither should ride along with shipping a code change - but both should be one
|
||||||
|
/// click when refreshing the data IS the intent, which is what this is.
|
||||||
|
///
|
||||||
|
/// It only prepares the inputs. Cutting a version from them stays a separate step, so a
|
||||||
|
/// refresh can be looked at before anything is published.
|
||||||
|
pub fn run_refresh(
|
||||||
|
mcp_repo: &Path,
|
||||||
|
work_dir: &Path,
|
||||||
|
pg_password: &str,
|
||||||
|
tile_root: Option<&str>,
|
||||||
|
sudo: &str,
|
||||||
|
console: &Shared,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let script = mcp_repo.join("scripts").join("refresh_data.ps1");
|
||||||
|
if !script.is_file() {
|
||||||
|
return Err(format!("refresh_data.ps1 is not in {}. Pull ODIAN_MCP_Server on this PC.", mcp_repo.join("scripts").display()));
|
||||||
|
}
|
||||||
|
let log = work_dir.join("refresh.log");
|
||||||
|
let mut body = String::new();
|
||||||
|
if !pg_password.is_empty() {
|
||||||
|
body.push_str(&format!(
|
||||||
|
"Write-Host '=== Database seed ===' -ForegroundColor Cyan
|
||||||
|
& '{}' -Seed -PgPassword $pgpw -NodeSudoPw $pw
|
||||||
|
if ($LASTEXITCODE -ne 0) {{ throw 'the seed refresh failed' }}
|
||||||
|
",
|
||||||
|
script.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(root) = tile_root.filter(|r| !r.trim().is_empty()) {
|
||||||
|
body.push_str(&format!(
|
||||||
|
"Write-Host '=== Tile indexes ===' -ForegroundColor Cyan
|
||||||
|
& '{}' -Tiles -TileRoot '{}' -NodeSudoPw $pw
|
||||||
|
if ($LASTEXITCODE -ne 0) {{ throw 'the tile refresh failed' }}
|
||||||
|
",
|
||||||
|
script.display(),
|
||||||
|
root.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if body.is_empty() {
|
||||||
|
return Err("Nothing to refresh: give the database password, a tile folder, or both.".to_string());
|
||||||
|
}
|
||||||
|
let ps = console_script(&body, &log, "Refresh finished. Nothing is published yet.");
|
||||||
|
spawn_console(work_dir, "refresh.ps1", &ps, &[("ODIAN_NODE_SUDO", sudo), ("ODIAN_PG_PW", pg_password)], console)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap a body so a step that throws cannot take the window with it, and everything said is
|
||||||
|
/// also on disk. No `pause`: this runs into the launcher's own panel, where there is nothing
|
||||||
|
/// to press Enter with, and the exit code carries the verdict instead.
|
||||||
|
fn console_script(body: &str, log: &Path, done: &str) -> String {
|
||||||
|
format!(
|
||||||
"$ErrorActionPreference = 'Stop'
|
"$ErrorActionPreference = 'Stop'
|
||||||
# The pipe gets UTF-8 or Windows' own French messages arrive as mojibake.
|
# The pipe gets UTF-8 or Windows' own French messages arrive as mojibake.
|
||||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
||||||
$pw = $env:ODIAN_NODE_SUDO
|
$pw = $env:ODIAN_NODE_SUDO
|
||||||
$env:ODIAN_NODE_SUDO = $null
|
$env:ODIAN_NODE_SUDO = $null
|
||||||
|
$pgpw = $env:ODIAN_PG_PW
|
||||||
|
$env:ODIAN_PG_PW = $null
|
||||||
Start-Transcript -Path '{}' -Force | Out-Null
|
Start-Transcript -Path '{}' -Force | Out-Null
|
||||||
$ok = $true
|
$ok = $true
|
||||||
try {{
|
try {{
|
||||||
{}
|
{}
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host 'Release finished.'
|
Write-Host '{}'
|
||||||
}} catch {{
|
}} catch {{
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host \"FAILED: $_\"
|
Write-Host \"FAILED: $_\"
|
||||||
@@ -466,16 +534,31 @@ if (-not $ok) {{ exit 1 }}
|
|||||||
",
|
",
|
||||||
log.display(),
|
log.display(),
|
||||||
body,
|
body,
|
||||||
|
done,
|
||||||
log.display()
|
log.display()
|
||||||
);
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write `ps` and run it with its output going to the console panel.
|
||||||
|
///
|
||||||
|
/// Secrets go in the ENVIRONMENT, never the command line: arguments are readable by anyone
|
||||||
|
/// who can list processes.
|
||||||
|
fn spawn_console(
|
||||||
|
work_dir: &Path,
|
||||||
|
name: &str,
|
||||||
|
ps: &str,
|
||||||
|
env: &[(&str, &str)],
|
||||||
|
console: &Shared,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
std::fs::create_dir_all(work_dir).map_err(|e| format!("cannot use {}: {e}", work_dir.display()))?;
|
std::fs::create_dir_all(work_dir).map_err(|e| format!("cannot use {}: {e}", work_dir.display()))?;
|
||||||
let file = work_dir.join("release.ps1");
|
let file = work_dir.join(name);
|
||||||
std::fs::write(&file, ps).map_err(|e| format!("cannot write {}: {e}", file.display()))?;
|
std::fs::write(&file, ps).map_err(|e| format!("cannot write {}: {e}", file.display()))?;
|
||||||
|
|
||||||
let mut cmd = Command::new("powershell");
|
let mut cmd = Command::new("powershell");
|
||||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"]).arg(&file);
|
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"]).arg(&file);
|
||||||
cmd.env("ODIAN_NODE_SUDO", sudo);
|
for (k, v) in env {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
// Nothing can type into this, so stdin is closed: a script that ever asks a question
|
// Nothing can type into this, so stdin is closed: a script that ever asks a question
|
||||||
// then fails at once instead of hanging forever with no window to answer in.
|
// then fails at once instead of hanging forever with no window to answer in.
|
||||||
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||||
@@ -587,7 +670,10 @@ mod tests {
|
|||||||
assert_eq!(d.tile_bytes, 100);
|
assert_eq!(d.tile_bytes, 100);
|
||||||
// Deduplicated, and the undated set does not drag the age to None.
|
// Deduplicated, and the undated set does not drag the age to None.
|
||||||
assert_eq!(d.tile_datasets, vec!["protomaps_20260423".to_string(), "planet_heightmap".to_string()]);
|
assert_eq!(d.tile_datasets, vec!["protomaps_20260423".to_string(), "planet_heightmap".to_string()]);
|
||||||
assert_eq!(d.tiles_age_days, Some(161));
|
// The dataset name carries the upstream extract's date, not a bake time - there is no
|
||||||
|
// bake time anywhere in a tile index or a manifest, which is why none is shown.
|
||||||
|
assert_eq!(d.basemap_vintage.as_deref(), Some("20260423"));
|
||||||
|
assert_eq!(d.basemap_vintage_days, Some(161));
|
||||||
|
|
||||||
// A manifest with no tiles at all must not panic or invent numbers.
|
// A manifest with no tiles at all must not panic or invent numbers.
|
||||||
let empty = data_from_manifest(&serde_json::json!({}), "0.1.0", false, now);
|
let empty = data_from_manifest(&serde_json::json!({}), "0.1.0", false, now);
|
||||||
|
|||||||
@@ -25,6 +25,21 @@
|
|||||||
/** The short version of a row's state, for the column that has room for three words.
|
/** The short version of a row's state, for the column that has room for three words.
|
||||||
NOT called `state`: that shadows the `$state` rune in this file, and everything
|
NOT called `state`: that shadows the `$state` rune in this file, and everything
|
||||||
declared with it silently stops being reactive. */
|
declared with it silently stops being reactive. */
|
||||||
|
// The refresh is its own press, with its own password: it dumps a database operators are
|
||||||
|
// using, so it must not be reachable by the same click that ships a code change.
|
||||||
|
let refreshOpen = $state(false);
|
||||||
|
let pgPassword = $state("");
|
||||||
|
let tileRoot = $state("");
|
||||||
|
const canRefresh = $derived(
|
||||||
|
password.trim().length > 0 && (pgPassword.trim().length > 0 || tileRoot.trim().length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
if (!canRefresh || busy) return;
|
||||||
|
await launcher.refreshRun(password, pgPassword, tileRoot);
|
||||||
|
pgPassword = "";
|
||||||
|
}
|
||||||
|
|
||||||
/** `20260921T125618Z` -> `21 Sep 2026`; anything else is shown as it came. */
|
/** `20260921T125618Z` -> `21 Sep 2026`; anything else is shown as it came. */
|
||||||
function stamped(snapshot: string | null): string {
|
function stamped(snapshot: string | null): string {
|
||||||
if (!snapshot || snapshot.length < 8) return snapshot ?? "unknown";
|
if (!snapshot || snapshot.length < 8) return snapshot ?? "unknown";
|
||||||
@@ -36,6 +51,13 @@
|
|||||||
return `${d} ${MONTHS[m - 1] ?? "?"} ${y}`;
|
return `${d} ${MONTHS[m - 1] ?? "?"} ${y}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `20260423` -> `Apr 2026`. The vintage of the extract, not an age. */
|
||||||
|
function monthOf(stamp: string): string {
|
||||||
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
const m = Number(stamp.slice(4, 6));
|
||||||
|
return `${MONTHS[m - 1] ?? "?"} ${stamp.slice(0, 4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function aged(days: number | null): string {
|
function aged(days: number | null): string {
|
||||||
if (days === null) return "";
|
if (days === null) return "";
|
||||||
if (days <= 0) return "today";
|
if (days <= 0) return "today";
|
||||||
@@ -97,10 +119,55 @@
|
|||||||
<div class="payload-row">
|
<div class="payload-row">
|
||||||
<span class="payload-name">Map tiles</span>
|
<span class="payload-name">Map tiles</span>
|
||||||
<span class="payload-val mono">{d.tile_sets} sets</span>
|
<span class="payload-val mono">{d.tile_sets} sets</span>
|
||||||
<span class="payload-age" class:stale={(d.tiles_age_days ?? 0) > 90}>{aged(d.tiles_age_days)}</span>
|
<!-- Deliberately NOT an age. Nothing records when tiles were baked, and the only
|
||||||
|
date available is the upstream extract's, which said "161 days old" about
|
||||||
|
work that is three weeks old. -->
|
||||||
|
<span class="payload-age" class:stale={(d.basemap_vintage_days ?? 0) > 365}>
|
||||||
|
{d.basemap_vintage ? `map data ${monthOf(d.basemap_vintage)}` : ""}
|
||||||
|
</span>
|
||||||
<span class="payload-size mono">{formatBytes(d.tile_bytes)}</span>
|
<span class="payload-size mono">{formatBytes(d.tile_bytes)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="payload-foot mono">{d.tile_datasets.join(" · ")}</span>
|
<span class="payload-foot mono">{d.tile_datasets.join(" · ")}</span>
|
||||||
|
<span class="payload-foot">
|
||||||
|
Tiles record no bake date, so only the source extract's vintage is known.
|
||||||
|
</span>
|
||||||
|
<div class="payload-act">
|
||||||
|
<button class="btn-link" onclick={() => (refreshOpen = !refreshOpen)}>
|
||||||
|
{refreshOpen ? "Never mind" : "Bring up to date…"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if refreshOpen}
|
||||||
|
<div class="refresh">
|
||||||
|
<p class="refresh-why">
|
||||||
|
This rebuilds the seed from the live database and re-indexes the tiles. It
|
||||||
|
<strong>publishes nothing</strong> — it prepares the inputs, and the console says how to cut a
|
||||||
|
version from them.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="database password (Data PC) — leave empty to skip the seed"
|
||||||
|
bind:value={pgPassword}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="tile folder laid out as style\dataset — leave empty to skip the tiles"
|
||||||
|
bind:value={tileRoot}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<div class="row">
|
||||||
|
<span class="refresh-note">
|
||||||
|
The seed is minutes of load on a database people are using. Uses the node password above.
|
||||||
|
</span>
|
||||||
|
<button class="btn" disabled={!canRefresh || busy} onclick={refresh}>Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if !d.is_live}
|
{#if !d.is_live}
|
||||||
<span class="payload-foot">Read from this PC's {d.version}, which is not what the DHP serves.</span>
|
<span class="payload-foot">Read from this PC's {d.version}, which is not what the DHP serves.</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -189,6 +256,33 @@
|
|||||||
.payload-age.stale {
|
.payload-age.stale {
|
||||||
color: var(--warn);
|
color: var(--warn);
|
||||||
}
|
}
|
||||||
|
.payload-act {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.refresh {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.refresh-why,
|
||||||
|
.refresh-note {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.refresh .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.refresh .row .refresh-note {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.payload-foot {
|
.payload-foot {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ function createTauriBackend(): Backend {
|
|||||||
releasePlan: () => invoke<ReleasePlan>("release_plan"),
|
releasePlan: () => invoke<ReleasePlan>("release_plan"),
|
||||||
releaseRun: (password, withApi, withMcp) => invoke<void>("release_run", { password, withApi, withMcp }),
|
releaseRun: (password, withApi, withMcp) => invoke<void>("release_run", { password, withApi, withMcp }),
|
||||||
releaseConsole: (since) => invoke<ReleaseConsoleTail>("release_console", { since }),
|
releaseConsole: (since) => invoke<ReleaseConsoleTail>("release_console", { since }),
|
||||||
|
refreshRun: (password, pgPassword, tileRoot) => invoke<void>("refresh_run", { password, pgPassword, tileRoot }),
|
||||||
onTileSyncProgress: (handler) => subscribe<TileSyncProgress>("tile-sync-progress", handler),
|
onTileSyncProgress: (handler) => subscribe<TileSyncProgress>("tile-sync-progress", handler),
|
||||||
onRemoveProgress: (handler) => subscribe<RemoveProgress>("remove-progress", handler),
|
onRemoveProgress: (handler) => subscribe<RemoveProgress>("remove-progress", handler),
|
||||||
onStack: (handler) => subscribe<StackEvent>("stack", handler),
|
onStack: (handler) => subscribe<StackEvent>("stack", handler),
|
||||||
|
|||||||
@@ -469,6 +469,20 @@ class Launcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rebuild the pack's data payload. Streams into the same console as a release. */
|
||||||
|
refreshRun(password: string, pgPassword: string, tileRoot: string) {
|
||||||
|
return this.run("release", async () => {
|
||||||
|
await backend.refreshRun(password, pgPassword, tileRoot);
|
||||||
|
this.releaseOpen = false;
|
||||||
|
this.consoleLines = [];
|
||||||
|
this.consoleTotal = 0;
|
||||||
|
this.consoleExit = null;
|
||||||
|
this.consoleRunning = true;
|
||||||
|
this.consoleOpen = true;
|
||||||
|
this.pollConsole();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Reopen it after it was closed; a finished release keeps its output to be read. */
|
/** Reopen it after it was closed; a finished release keeps its output to be read. */
|
||||||
showConsole() {
|
showConsole() {
|
||||||
this.consoleOpen = true;
|
this.consoleOpen = true;
|
||||||
|
|||||||
+21
-1
@@ -825,7 +825,8 @@ class MockBackend implements Backend {
|
|||||||
tile_sets: 8,
|
tile_sets: 8,
|
||||||
tile_bytes: 102_650_000_000,
|
tile_bytes: 102_650_000_000,
|
||||||
tile_datasets: ["protomaps_20260423", "planet_heightmap"],
|
tile_datasets: ["protomaps_20260423", "planet_heightmap"],
|
||||||
tiles_age_days: 161,
|
basemap_vintage: "20260423",
|
||||||
|
basemap_vintage_days: 161,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -854,6 +855,25 @@ class MockBackend implements Backend {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshRun(password: string, pgPassword: string, tileRoot: string): Promise<void> {
|
||||||
|
await this.wait(300);
|
||||||
|
if (!password.trim()) throw "The node's sudo password is needed: the dump runs there.";
|
||||||
|
if (!pgPassword.trim() && !tileRoot.trim()) throw "Nothing to refresh: give the database password, a tile folder, or both.";
|
||||||
|
this.consoleLines = ["=== Database seed ==="];
|
||||||
|
this.consoleDone = null;
|
||||||
|
const rest = pgPassword.trim()
|
||||||
|
? ["live version : 0.75.0", "dumping odian from 192.168.1.13 ...", "snapshot : 20261001T090000Z (was 20260921T125618Z)",
|
||||||
|
"tables : 302 bytes: 15.40 GB", "Refresh finished. Nothing is published yet."]
|
||||||
|
: ["=== Tile indexes ===", "re-indexing 8 sets...", "Refresh finished. Nothing is published yet."];
|
||||||
|
void (async () => {
|
||||||
|
for (const line of rest) {
|
||||||
|
await this.wait(600);
|
||||||
|
this.consoleLines.push(line);
|
||||||
|
}
|
||||||
|
this.consoleDone = 0;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
private consoleLines: string[] = [];
|
private consoleLines: string[] = [];
|
||||||
private consoleDone: number | null = 0;
|
private consoleDone: number | null = 0;
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -314,8 +314,10 @@ export interface ReleaseData {
|
|||||||
tile_sets: number;
|
tile_sets: number;
|
||||||
tile_bytes: number;
|
tile_bytes: number;
|
||||||
tile_datasets: string[];
|
tile_datasets: string[];
|
||||||
/** Age of the oldest dated dataset; undated sets (heightmap) are not counted. */
|
/** The upstream extract the basemap was drawn from, `YYYYMMDD`. NOT a bake time: tiles
|
||||||
tiles_age_days: number | null;
|
* record none anywhere, so there is no "how old are our tiles" to show. */
|
||||||
|
basemap_vintage: string | null;
|
||||||
|
basemap_vintage_days: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReleasePlan {
|
export interface ReleasePlan {
|
||||||
@@ -412,6 +414,8 @@ export interface Backend {
|
|||||||
releasePlan(): Promise<ReleasePlan>;
|
releasePlan(): Promise<ReleasePlan>;
|
||||||
releaseRun(password: string, withApi: boolean, withMcp: boolean): Promise<void>;
|
releaseRun(password: string, withApi: boolean, withMcp: boolean): Promise<void>;
|
||||||
releaseConsole(since: number): Promise<ReleaseConsoleTail>;
|
releaseConsole(since: number): Promise<ReleaseConsoleTail>;
|
||||||
|
/** Rebuild the pack's data payload. Prepares inputs only; it publishes nothing. */
|
||||||
|
refreshRun(password: string, pgPassword: string, tileRoot: string): Promise<void>;
|
||||||
/** What the operator has to decide, oldest first. */
|
/** What the operator has to decide, oldest first. */
|
||||||
getSyncConflicts(): Promise<SyncConflict[]>;
|
getSyncConflicts(): Promise<SyncConflict[]>;
|
||||||
/** Records one decision; resolves with what is left to decide. */
|
/** Records one decision; resolves with what is left to decide. */
|
||||||
|
|||||||
Reference in New Issue
Block a user