feat: add live replays and PostgreSQL maintenance tools
Build and Push Docker Container / build-and-push (push) Successful in 7m53s
Build and Push Docker Container / build-and-push (push) Successful in 7m53s
- Stream compact live replay updates across local and clustered dashboards. - Render responsive snake bodies as SVG paths with aligned custom icons. - Add cache-busted assets, replay fallback routes, and live-follow playback. - Support PostgreSQL benchmark sampling and idempotent SQLite migration. - Add dry-run cleanup for old low-quality PostgreSQL replay payloads. - Reward safe perimeter lanes and bump Prism to version 1.5.0. - Add backend, migration, dashboard, and perimeter regression coverage.
This commit is contained in:
@@ -262,11 +262,12 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 8px;
|
||||
min-height: 520px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.legend {
|
||||
@@ -291,54 +292,38 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.board {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
flex: none;
|
||||
background: var(--grid);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
align-content: start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Snake bodies are drawn as one rounded polyline per snake on top of the grid,
|
||||
so bends and cell gaps need no per-cell patching. */
|
||||
.snake-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cell {
|
||||
background: var(--cell);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.snake-turn-cell::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--turn-color, transparent);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 50% = quarter-circle at the inner corner of the bend */
|
||||
.snake-turn-cell.snake-turn-ur::after {
|
||||
border-top-right-radius: 50%;
|
||||
}
|
||||
|
||||
.snake-turn-cell.snake-turn-ul::after {
|
||||
border-top-left-radius: 50%;
|
||||
}
|
||||
|
||||
.snake-turn-cell.snake-turn-dr::after {
|
||||
border-bottom-right-radius: 50%;
|
||||
}
|
||||
|
||||
.snake-turn-cell.snake-turn-dl::after {
|
||||
border-bottom-left-radius: 50%;
|
||||
}
|
||||
|
||||
.food {
|
||||
background-image: radial-gradient(circle at center, #d73a31 0 45%, transparent 48%);
|
||||
background-repeat: no-repeat;
|
||||
@@ -350,6 +335,12 @@ input[type="range"] {
|
||||
background-color: var(--hazard);
|
||||
}
|
||||
|
||||
/* The hatch overlay (::before, z-index 4) still paints above the body stroke,
|
||||
but the opaque fill would hide it. */
|
||||
.hazard.hazard-over-snake {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.hazard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -364,17 +355,11 @@ input[type="range"] {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.snake-you {
|
||||
background: var(--you);
|
||||
}
|
||||
|
||||
.snake-enemy {
|
||||
background: var(--enemy);
|
||||
}
|
||||
|
||||
/* Head/tail markers and icon layers use z-index >= 2 so they paint above
|
||||
.snake-layer. The cells themselves stay unstacked, keeping their background
|
||||
below the body stroke. */
|
||||
.snake-head {
|
||||
outline: 2px solid var(--head-ring);
|
||||
outline-offset: -2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.snake-head::after {
|
||||
@@ -494,43 +479,41 @@ input[type="range"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.snake-tail-you.has-tail-icon,
|
||||
.snake-tail-enemy.has-tail-icon {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Spans the full cell along the travel axis and the body stroke width across
|
||||
it, so the icon is exactly as thick as the body and its leading edge lands on
|
||||
the cell border where the stroke ends. */
|
||||
.icon-layer {
|
||||
position: absolute;
|
||||
inset: 2%;
|
||||
background: var(--icon-color, currentColor);
|
||||
-webkit-mask-image: var(--icon-url);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-image: var(--icon-url);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--icon-cross-inset, 7%);
|
||||
bottom: var(--icon-cross-inset, 7%);
|
||||
transform: var(--icon-transform, rotate(0deg));
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.icon-layer--tail {
|
||||
z-index: 2;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.icon-layer--head {
|
||||
z-index: 3;
|
||||
opacity: 1;
|
||||
background: none;
|
||||
-webkit-mask-image: none;
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.icon-layer--head>svg {
|
||||
/* Fallback for artwork that has not been fetched yet. An SVG used as a mask
|
||||
image keeps its own preserveAspectRatio and letterboxes inside this
|
||||
non-square box, so the inlined variant above is preferred. */
|
||||
.icon-layer--masked {
|
||||
background: var(--icon-color, currentColor);
|
||||
-webkit-mask-image: var(--icon-url);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-image: var(--icon-url);
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.icon-layer>svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
@@ -693,6 +676,19 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
html,
|
||||
body {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -704,6 +700,12 @@ input[type="range"] {
|
||||
|
||||
.main {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.right {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.games {
|
||||
@@ -714,8 +716,12 @@ input[type="range"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.thinking {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
min-height: 360px;
|
||||
min-height: min(70vw, 520px);
|
||||
}
|
||||
|
||||
.turn-badge {
|
||||
|
||||
Reference in New Issue
Block a user