feat(fed): add ural
This commit is contained in:
@@ -13,7 +13,7 @@ ASP.NET Core сервер векторных тайлов для клиенто
|
|||||||
- .NET 8 SDK
|
- .NET 8 SDK
|
||||||
- **Java 21+** в `PATH` (или путь в `TileServer:JavaPath`) — без Java тайлы не соберутся, уже готовые MBTiles сервер всё равно отдаст
|
- **Java 21+** в `PATH` (или путь в `TileServer:JavaPath`) — без Java тайлы не соберутся, уже готовые MBTiles сервер всё равно отдаст
|
||||||
- Диск: 5–10× размер PBF. ЦФО до **z15** — ориентир несколько ГБ готового MBTiles плюс столько же на staging во время сборки
|
- Диск: 5–10× размер PBF. ЦФО до **z15** — ориентир несколько ГБ готового MBTiles плюс столько же на staging во время сборки
|
||||||
- RAM: ориентир `TileServer:JvmMaxHeap` ≈ 0.5× размер PBF (по умолчанию `8g`)
|
- RAM: контейнер ~12g. `TileServer:JvmMaxHeap` — только Java heap, держите **~⅓ лимита** (по умолчанию `4g`). Остальное — mmap/файлы Planetiler и Kestrel. Код **137** = OOM killer, не переполнение `-Xmx`.
|
||||||
|
|
||||||
Тайлы собираются через OpenMapTiles. Схема рассчитана на z0–z14 (дефолт MapTiler/Planetiler). Жёсткий потолок **planetiler-openmaptiles 3.16 — z15**; z16+ этот JAR не умеет. Ближе z15 MapLibre overzoom’ит те же тайлы до z18.
|
Тайлы собираются через OpenMapTiles. Схема рассчитана на z0–z14 (дефолт MapTiler/Planetiler). Жёсткий потолок **planetiler-openmaptiles 3.16 — z15**; z16+ этот JAR не умеет. Ближе z15 MapLibre overzoom’ит те же тайлы до z18.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 2026-09-21 — СЗФО Failed, Planetiler 137
|
||||||
|
|
||||||
|
Баннер «ещё не готовы (ошибка)» — не «ещё качается». В логе:
|
||||||
|
|
||||||
|
`Planetiler exited with code 137` на `northwestern-fed-district` при z15 ~91% (38M tiles / 3.6G, heap 4.7G/8.5G). 137 = SIGKILL от cgroup OOM killer.
|
||||||
|
|
||||||
|
Причина: контейнер `mem_limit: 12g` без swap, JVM `-Xmx8g`. Heap + mmap `feature.db` (~6G) + staging mbtiles + Kestrel > 12g.
|
||||||
|
|
||||||
|
Фикс: heap `4g`, `--threads=8`, `--mmap_temp=false`, повтор Failed и один retry на 137, баннер показывает `lastError`. После деплоя: `docker compose up -d --build`, затем `POST /api/v1/sync/northwestern-fed-district` если RunOnStartup уже прошёл.
|
||||||
+5
-2
@@ -13,7 +13,8 @@
|
|||||||
# docker run --rm -v "$(pwd)/data:/from:ro" -v tile-server-data:/to alpine \
|
# docker run --rm -v "$(pwd)/data:/from:ro" -v tile-server-data:/to alpine \
|
||||||
# sh -c "cp -a /from/. /to/"
|
# sh -c "cp -a /from/. /to/"
|
||||||
#
|
#
|
||||||
# Tune TileServer__JvmMaxHeap / mem_limit to the machine.
|
# Heap must stay well below mem_limit: Planetiler maps temp files outside -Xmx.
|
||||||
|
# Exit 137 = cgroup OOM killer. 8g heap in a 12g box dies on NW FO z15.
|
||||||
# After raising MaxZoom (z14→z15) the next startup rebuilds MBTiles; keep the volume.
|
# After raising MaxZoom (z14→z15) the next startup rebuilds MBTiles; keep the volume.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -57,7 +58,9 @@ services:
|
|||||||
TZ: UTC
|
TZ: UTC
|
||||||
TileServer__DataDirectory: /data
|
TileServer__DataDirectory: /data
|
||||||
TileServer__JavaPath: /opt/java/openjdk/bin/java
|
TileServer__JavaPath: /opt/java/openjdk/bin/java
|
||||||
TileServer__JvmMaxHeap: 8g
|
TileServer__JvmMaxHeap: 4g
|
||||||
|
TileServer__PlanetilerThreads: "8"
|
||||||
|
TileServer__PlanetilerMmapTemp: "false"
|
||||||
TileServer__Sync__RunOnStartup: "true"
|
TileServer__Sync__RunOnStartup: "true"
|
||||||
TileServer__Sync__Cron: "0 3 * * *"
|
TileServer__Sync__Cron: "0 3 * * *"
|
||||||
TileServer__Sync__TimeZone: UTC
|
TileServer__Sync__TimeZone: UTC
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
"TileServer": {
|
"TileServer": {
|
||||||
"DataDirectory": "../../data",
|
"DataDirectory": "../../data",
|
||||||
"JavaPath": "java",
|
"JavaPath": "java",
|
||||||
"JvmMaxHeap": "8g",
|
"JvmMaxHeap": "4g",
|
||||||
|
"PlanetilerThreads": 8,
|
||||||
|
"PlanetilerMmapTemp": false,
|
||||||
"PlanetilerJarUrl": "https://github.com/openmaptiles/planetiler-openmaptiles/releases/download/v3.16/planetiler-openmaptiles.jar",
|
"PlanetilerJarUrl": "https://github.com/openmaptiles/planetiler-openmaptiles/releases/download/v3.16/planetiler-openmaptiles.jar",
|
||||||
"PlanetilerHttpTimeout": "15m",
|
"PlanetilerHttpTimeout": "15m",
|
||||||
"DownloadStallTimeoutSeconds": 45,
|
"DownloadStallTimeoutSeconds": 45,
|
||||||
@@ -65,6 +67,15 @@
|
|||||||
"MinZoom": 0,
|
"MinZoom": 0,
|
||||||
"MaxZoom": 15,
|
"MaxZoom": 15,
|
||||||
"Enabled": true
|
"Enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "ural-fed-district",
|
||||||
|
"Name": "Уральский федеральный округ",
|
||||||
|
"Url": "https://download.geofabrik.de/russia/ural-fed-district-latest.osm.pbf",
|
||||||
|
"Center": [60.0, 55.0],
|
||||||
|
"MinZoom": 0,
|
||||||
|
"MaxZoom": 15,
|
||||||
|
"Enabled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,20 @@ public sealed class TileServerOptions
|
|||||||
|
|
||||||
public string JavaPath { get; set; } = "java";
|
public string JavaPath { get; set; } = "java";
|
||||||
|
|
||||||
public string JvmMaxHeap { get; set; } = "8g";
|
/// <summary>
|
||||||
|
/// JVM heap only. Planetiler memory-maps temp features outside this budget.
|
||||||
|
/// Keep around one third of the container mem_limit (4g in a 12g box).
|
||||||
|
/// Exit 137 is the OOM killer, not a Java heap dump.
|
||||||
|
/// </summary>
|
||||||
|
public string JvmMaxHeap { get; set; } = "4g";
|
||||||
|
|
||||||
|
/// <summary>0 = Planetiler default (all CPUs). Cap on small containers to limit RSS.</summary>
|
||||||
|
public int PlanetilerThreads { get; set; } = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// mmap of feature.db inflates RSS on large z15 extracts. Off by default for 12g hosts.
|
||||||
|
/// </summary>
|
||||||
|
public bool PlanetilerMmapTemp { get; set; }
|
||||||
|
|
||||||
public string PlanetilerJarUrl { get; set; } =
|
public string PlanetilerJarUrl { get; set; } =
|
||||||
"https://github.com/openmaptiles/planetiler-openmaptiles/releases/download/v3.16/planetiler-openmaptiles.jar";
|
"https://github.com/openmaptiles/planetiler-openmaptiles/releases/download/v3.16/planetiler-openmaptiles.jar";
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ public sealed class MapSyncService(
|
|||||||
{
|
{
|
||||||
var state = await stateStore.GetAsync(extract.Id, ct).ConfigureAwait(false);
|
var state = await stateStore.GetAsync(extract.Id, ct).ConfigureAwait(false);
|
||||||
var pbfPath = dataPaths.OsmPbf(extract.Id);
|
var pbfPath = dataPaths.OsmPbf(extract.Id);
|
||||||
|
var previousStatus = state.Status;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -150,6 +151,7 @@ public sealed class MapSyncService(
|
|||||||
var metadata = tileStore.GetMetadata(extract.Id);
|
var metadata = tileStore.GetMetadata(extract.Id);
|
||||||
var zoomRaised = metadata is not null && extract.MaxZoom > metadata.MaxZoom;
|
var zoomRaised = metadata is not null && extract.MaxZoom > metadata.MaxZoom;
|
||||||
var shouldBuild = download.Changed
|
var shouldBuild = download.Changed
|
||||||
|
|| previousStatus == ExtractSyncState.Failed
|
||||||
|| (tilesMissing && options.Value.Sync.RebuildIfTilesMissing)
|
|| (tilesMissing && options.Value.Sync.RebuildIfTilesMissing)
|
||||||
|| zoomRaised;
|
|| zoomRaised;
|
||||||
if (!shouldBuild)
|
if (!shouldBuild)
|
||||||
@@ -168,6 +170,10 @@ public sealed class MapSyncService(
|
|||||||
extract.MaxZoom,
|
extract.MaxZoom,
|
||||||
metadata!.MaxZoom);
|
metadata!.MaxZoom);
|
||||||
}
|
}
|
||||||
|
else if (previousStatus == ExtractSyncState.Failed)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Rebuilding {ExtractId}: previous run failed.", extract.Id);
|
||||||
|
}
|
||||||
|
|
||||||
if (!planetiler.IsJavaAvailable())
|
if (!planetiler.IsJavaAvailable())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TileServer.Infrastructure.Tiles;
|
||||||
|
|
||||||
|
public static class PlanetilerExit
|
||||||
|
{
|
||||||
|
/// <summary>128 + SIGKILL. Docker/cgroup OOM killer, not a Java heap dump.</summary>
|
||||||
|
public const int LinuxOomKill = 137;
|
||||||
|
|
||||||
|
public static bool IsLikelyOom(int exitCode) => exitCode is LinuxOomKill or 9;
|
||||||
|
|
||||||
|
public static string Describe(int exitCode, string extractId)
|
||||||
|
{
|
||||||
|
if (IsLikelyOom(exitCode))
|
||||||
|
{
|
||||||
|
return
|
||||||
|
$"Planetiler was killed (exit {exitCode}, typically the Linux OOM killer) while building '{extractId}'. " +
|
||||||
|
"JVM -Xmx is only the heap; Planetiler also maps temp features. " +
|
||||||
|
"Keep TileServer:JvmMaxHeap around 1/3 of the container mem_limit (4g in 12g) and PlanetilerMmapTemp=false.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Planetiler exited with code {exitCode} for extract '{extractId}'.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ public sealed class PlanetilerTileBuilder(
|
|||||||
IOptions<TileServerOptions> options,
|
IOptions<TileServerOptions> options,
|
||||||
ILogger<PlanetilerTileBuilder> logger) : ITileBuilder
|
ILogger<PlanetilerTileBuilder> logger) : ITileBuilder
|
||||||
{
|
{
|
||||||
|
private const int OomRetryAttempts = 2;
|
||||||
|
|
||||||
public async Task BuildAsync(Extract extract, string osmPbfPath, string outputMbtilesPath, CancellationToken ct)
|
public async Task BuildAsync(Extract extract, string osmPbfPath, string outputMbtilesPath, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!File.Exists(osmPbfPath))
|
if (!File.Exists(osmPbfPath))
|
||||||
@@ -24,16 +26,64 @@ public sealed class PlanetilerTileBuilder(
|
|||||||
|
|
||||||
var jar = await bootstrapper.EnsureJarAsync(ct).ConfigureAwait(false);
|
var jar = await bootstrapper.EnsureJarAsync(ct).ConfigureAwait(false);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputMbtilesPath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(outputMbtilesPath)!);
|
||||||
|
Directory.CreateDirectory(dataPaths.Tmp);
|
||||||
|
|
||||||
|
var cfg = options.Value;
|
||||||
|
Exception? lastFailure = null;
|
||||||
|
for (var attempt = 1; attempt <= OomRetryAttempts; attempt++)
|
||||||
|
{
|
||||||
if (File.Exists(outputMbtilesPath))
|
if (File.Exists(outputMbtilesPath))
|
||||||
{
|
{
|
||||||
File.Delete(outputMbtilesPath);
|
File.Delete(outputMbtilesPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg = options.Value;
|
var psi = CreateStartInfo(cfg, jar, extract, osmPbfPath, outputMbtilesPath);
|
||||||
var java = cfg.JavaPath;
|
logger.LogInformation(
|
||||||
|
"Starting Planetiler for {ExtractId} (attempt {Attempt}/{Max}): {File} {Args}",
|
||||||
|
extract.Id,
|
||||||
|
attempt,
|
||||||
|
OomRetryAttempts,
|
||||||
|
cfg.JavaPath,
|
||||||
|
string.Join(' ', psi.ArgumentList));
|
||||||
|
|
||||||
|
var exitCode = await RunAsync(psi, extract.Id, ct).ConfigureAwait(false);
|
||||||
|
ClearTmp();
|
||||||
|
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
if (!File.Exists(outputMbtilesPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Planetiler finished but '{outputMbtilesPath}' was not created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFailure = new InvalidOperationException(PlanetilerExit.Describe(exitCode, extract.Id));
|
||||||
|
if (!PlanetilerExit.IsLikelyOom(exitCode) || attempt == OomRetryAttempts)
|
||||||
|
{
|
||||||
|
throw lastFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogWarning(
|
||||||
|
lastFailure,
|
||||||
|
"Planetiler OOM for {ExtractId}; retrying after clearing tmp.",
|
||||||
|
extract.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastFailure ?? new InvalidOperationException($"Planetiler failed for extract '{extract.Id}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProcessStartInfo CreateStartInfo(
|
||||||
|
TileServerOptions cfg,
|
||||||
|
string jar,
|
||||||
|
Extract extract,
|
||||||
|
string osmPbfPath,
|
||||||
|
string outputMbtilesPath)
|
||||||
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = java,
|
FileName = cfg.JavaPath,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
@@ -59,28 +109,36 @@ public sealed class PlanetilerTileBuilder(
|
|||||||
psi.ArgumentList.Add($"--natural_earth_url={cfg.NaturalEarthUrl}");
|
psi.ArgumentList.Add($"--natural_earth_url={cfg.NaturalEarthUrl}");
|
||||||
psi.ArgumentList.Add($"--lake_centerlines_url={cfg.LakeCenterlinesUrl}");
|
psi.ArgumentList.Add($"--lake_centerlines_url={cfg.LakeCenterlinesUrl}");
|
||||||
psi.ArgumentList.Add($"--water_polygons_url={cfg.WaterPolygonsUrl}");
|
psi.ArgumentList.Add($"--water_polygons_url={cfg.WaterPolygonsUrl}");
|
||||||
|
psi.ArgumentList.Add($"--mmap_temp={cfg.PlanetilerMmapTemp.ToString().ToLowerInvariant()}");
|
||||||
|
if (cfg.PlanetilerThreads > 0)
|
||||||
|
{
|
||||||
|
psi.ArgumentList.Add($"--threads={cfg.PlanetilerThreads}");
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("Starting Planetiler for {ExtractId}: {File} {Args}", extract.Id, java, string.Join(' ', psi.ArgumentList));
|
return psi;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> RunAsync(ProcessStartInfo psi, string extractId, CancellationToken ct)
|
||||||
|
{
|
||||||
using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
using var process = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||||
process.OutputDataReceived += (_, e) =>
|
process.OutputDataReceived += (_, e) =>
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(e.Data))
|
if (!string.IsNullOrWhiteSpace(e.Data))
|
||||||
{
|
{
|
||||||
logger.LogInformation("[planetiler {ExtractId}] {Line}", extract.Id, e.Data);
|
logger.LogInformation("[planetiler {ExtractId}] {Line}", extractId, e.Data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
process.ErrorDataReceived += (_, e) =>
|
process.ErrorDataReceived += (_, e) =>
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(e.Data))
|
if (!string.IsNullOrWhiteSpace(e.Data))
|
||||||
{
|
{
|
||||||
logger.LogInformation("[planetiler {ExtractId}] {Line}", extract.Id, e.Data);
|
logger.LogInformation("[planetiler {ExtractId}] {Line}", extractId, e.Data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!process.Start())
|
if (!process.Start())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Failed to start '{java}'.");
|
throw new InvalidOperationException($"Failed to start '{psi.FileName}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
@@ -96,14 +154,31 @@ public sealed class PlanetilerTileBuilder(
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.ExitCode != 0)
|
return process.ExitCode;
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Planetiler exited with code {process.ExitCode} for extract '{extract.Id}'.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(outputMbtilesPath))
|
private void ClearTmp()
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Planetiler finished but '{outputMbtilesPath}' was not created.");
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dataPaths.Tmp))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(dataPaths.Tmp))
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dir in Directory.EnumerateDirectories(dataPaths.Tmp))
|
||||||
|
{
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Could not clear Planetiler tmp {Tmp}", dataPaths.Tmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using TileServer.Infrastructure.Tiles;
|
||||||
|
|
||||||
|
namespace TileServer.UnitTests;
|
||||||
|
|
||||||
|
public sealed class PlanetilerExitTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsLikelyOom_DetectsLinuxOomKiller()
|
||||||
|
{
|
||||||
|
Assert.True(PlanetilerExit.IsLikelyOom(137));
|
||||||
|
Assert.True(PlanetilerExit.IsLikelyOom(9));
|
||||||
|
Assert.False(PlanetilerExit.IsLikelyOom(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Describe_Oom_MentionsHeapVersusRss()
|
||||||
|
{
|
||||||
|
var text = PlanetilerExit.Describe(137, "northwestern-fed-district");
|
||||||
|
Assert.Contains("northwestern-fed-district", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("OOM", text, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("JvmMaxHeap", text, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,8 +23,14 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
Failed: "ошибка"
|
Failed: "ошибка"
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatStatus(value?: string): string {
|
function sourceBanner(item: SourceItem): string {
|
||||||
return STATUS_LABELS[value ?? ""] ?? value ?? "—";
|
if (item.status === "Failed") {
|
||||||
|
const detail = item.lastError?.trim();
|
||||||
|
return detail
|
||||||
|
? `Сборка «${item.name}» не удалась: ${detail}`
|
||||||
|
: `Сборка «${item.name}» не удалась. Повтор — рестарт сервиса или POST /api/v1/sync/${item.id}.`;
|
||||||
|
}
|
||||||
|
return `Тайлы источника «${item.name}» ещё не готовы (${formatStatus(item.status)}). Первый прогон может занять часы.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value?: string | null): string {
|
function formatDate(value?: string | null): string {
|
||||||
@@ -179,11 +185,7 @@ export function App() {
|
|||||||
|
|
||||||
styleKeyRef.current = styleKey;
|
styleKeyRef.current = styleKey;
|
||||||
const pending = selectedSources.find((item) => item.status !== "Ready");
|
const pending = selectedSources.find((item) => item.status !== "Ready");
|
||||||
setBanner(
|
setBanner(pending ? sourceBanner(pending) : "");
|
||||||
pending
|
|
||||||
? `Тайлы источника «${pending.name}» ещё не готовы (${formatStatus(pending.status)}). Первый прогон может занять часы.`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
}, [access, personal, selectedSourceIds, sources, source, styleName, syncGotoFromMap, updateHud]);
|
}, [access, personal, selectedSourceIds, sources, source, styleName, syncGotoFromMap, updateHud]);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
|
|||||||
@@ -444,8 +444,8 @@ dd {
|
|||||||
.banner {
|
.banner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--hud-z);
|
z-index: var(--hud-z);
|
||||||
left: var(--space);
|
right: var(--space);
|
||||||
top: 5.4rem;
|
top: 10em;
|
||||||
max-width: min(34rem, calc(100vw - 24px));
|
max-width: min(34rem, calc(100vw - 24px));
|
||||||
background: rgba(42, 31, 18, 0.92);
|
background: rgba(42, 31, 18, 0.92);
|
||||||
color: #ffe0c2;
|
color: #ffe0c2;
|
||||||
|
|||||||
Reference in New Issue
Block a user