Refine production workspace layouts

This commit is contained in:
b0txec
2026-08-19 10:38:25 +03:00
parent 9147342686
commit fd1c0148b6
5 changed files with 95 additions and 37 deletions
+7 -28
View File
@@ -4,8 +4,9 @@
| Environment | Purpose | Address |
|---|---|---|
| Windows | Source editing, local Docker build, and initial testing | `http://localhost:9190` |
| Rocky Linux | Production-like isolated staging | `http://192.168.1.101:9190` |
| Windows | Source editing and Git workflow only | Not applicable |
| Rocky Linux | Docker development and production-like staging | `http://192.168.1.101:9190` |
| Future VPS | Planned deployment environment | Not provisioned yet |
| Workplace production | Out of scope until reviewed and approved | Not documented here |
Rocky checkout:
@@ -37,30 +38,9 @@ codex/staging-baseline
## Windows development
Build the frontend using the project Node container:
```powershell
docker compose run --rm node
```
Start or update the complete local stack when backend or Compose changes require it:
```powershell
docker compose up --build -d
```
Seed deterministic observations:
```powershell
docker compose --profile tools run --rm seed
```
Inspect status and logs:
```powershell
docker compose ps
docker compose logs --tail 100 scala
```
Use Windows to edit and review source files and to create Git commits. Docker builds,
the running application, synthetic-data operations, and browser testing belong on
Rocky. Docker Desktop is not part of the normal project workflow.
## Commit and transfer workflow
@@ -126,9 +106,8 @@ Prefer a normal Git revert rather than manually copying old files:
1. Identify the faulty commit with `git log --oneline`.
2. On the development checkout, run `git revert <commit>`.
3. Build and test the reverted state locally.
3. Push the revert and build and test it on Rocky.
4. Push the revert commit.
5. Pull it on Rocky and run the appropriate frontend-only or full-stack deployment command.
Database rollback is a separate operation and must not be inferred from a source rollback. Database backup and restore procedures are not yet defined.
+45
View File
@@ -14,4 +14,49 @@
}
.overlayFields{display:grid;grid-template-columns:minmax(260px,2fr) minmax(160px,1fr);gap:12px;padding:16px 16px 0}.overlayFields label,.overlayFields span{display:block}.overlayFields span{margin-bottom:6px;font-size:12px;font-weight:700}.overlayFields input{width:100%}
.mapControlSections{display:grid;gap:18px;padding:16px}.valueControls{padding-bottom:16px;border-bottom:1px solid var(--border)}
/* Workspace layout: controls explain the content without enclosing every region. */
.aggregator>.container{
grid-template-columns:230px minmax(0,1fr);
grid-template-areas:"cities query" "cities results";
column-gap:28px;
row-gap:18px;
}
.aggregator .workspaceRail{
grid-area:cities;
padding:4px 24px 10px 0;
border-right:1px solid rgba(90,126,166,.2);
}
.aggregator .workspaceRail h2{margin:0 0 18px;font-size:16px}
.aggregator .workspaceToolbar{
grid-area:query;
display:grid;
grid-template-columns:minmax(330px,1.15fr) minmax(480px,1.85fr);
gap:14px 28px;
align-items:end;
padding:2px 0 20px;
border-bottom:1px solid rgba(90,126,166,.22);
}
.aggregator .workspaceToolbar>h2{grid-column:1/-1;margin:0;font-size:18px}
.aggregator .queryOptions{
display:grid;
grid-template-columns:minmax(130px,1fr) minmax(125px,1.05fr) minmax(115px,.8fr);
gap:20px;
align-items:end;
}
.aggregator .queryPanel h4{margin:0 0 9px}
.aggregator .queryPanel p{margin:8px 0 0}
.aggregator .queryOptions ul{margin:0;padding:0}
.aggregator .resultsPanel{grid-area:results}
@media(max-width:1200px){
.aggregator>.container{grid-template-columns:210px minmax(0,1fr);grid-template-areas:"cities query" "cities results"}
.aggregator .workspaceToolbar{grid-template-columns:1fr}
.aggregator .workspaceToolbar>h2{grid-column:auto}
}
@media(max-width:800px){
.aggregator>.container{grid-template-columns:1fr;grid-template-areas:"cities" "query" "results"}
.aggregator .workspaceRail{padding-right:0;border-right:0;border-bottom:1px solid rgba(90,126,166,.2)}
.aggregator .queryOptions{grid-template-columns:1fr}
}
.pageWorkspace{max-width:1800px;margin:auto;padding:34px 28px 70px}.pageHeading{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:24px}.pageHeading h1{margin:5px 0 6px;font-size:34px}.pageHeading p{margin:0;color:var(--text-muted)}.selectionCount{padding:10px 14px;border:1px solid var(--border);border-radius:10px;background:var(--surface);font-weight:700}.aggregator>.container{display:grid;grid-template-columns:240px 300px minmax(0,1fr);gap:18px;align-items:start}.panel{padding:20px;border:1px solid var(--border);border-radius:16px;background:var(--surface);box-shadow:var(--shadow-sm)}.panel h2{margin:0 0 18px;font-size:16px}.cityPanel{max-height:calc(100vh - 160px);overflow:auto}.cityPanel ul{margin:12px 0;padding:0;columns:1}.cityPanel li{padding:3px 0}.queryPanel h4{margin:22px 0 9px}.queryPanel p{margin:10px 0}.resultsPanel{min-width:0}.emptyState{display:flex;min-height:260px;align-items:center;justify-content:center;flex-direction:column;gap:8px;margin-top:18px;padding:30px;border:1px dashed #bac6d4;border-radius:16px;background:rgba(255,255,255,.55);color:var(--text-muted);text-align:center}.emptyState strong{color:var(--text);font-size:18px}.mapWorkspace{margin-top:14px}.mapToolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.mapToolbar strong,.mapToolbar span{display:block}.mapToolbar span{margin-top:3px;color:var(--text-muted);font-size:12px}.broadcastControls{margin-bottom:14px;border:1px solid var(--border);border-radius:12px;background:var(--surface)}.broadcastControls summary{padding:14px 16px;cursor:pointer}.broadcastControls summary span,.broadcastControls summary strong,.broadcastControls summary small{display:block}.broadcastControls summary small,.controlHelp,.fieldHint{color:var(--text-muted);font-size:12px}.broadcastControls[open] summary{border-bottom:1px solid var(--border)}.broadcastControls .grid-1-2{padding:16px}.controlHelp{margin:16px 16px 0}.canvasFrame{overflow:hidden;border:1px solid var(--border);border-radius:14px;background:#d8dee6;box-shadow:var(--shadow-sm)}.canvasFrame canvas{display:block}.weatherIconButton{border-radius:7px}.fieldHint{margin:4px 0 10px}@media(max-width:1200px){.aggregator>.container{grid-template-columns:220px 1fr}.resultsPanel{grid-column:1/-1}.cityPanel{max-height:none}}@media(max-width:700px){.pageWorkspace{padding:24px 14px}.pageHeading{align-items:start;flex-direction:column}.aggregator>.container{grid-template-columns:1fr}.resultsPanel{grid-column:auto}}
+5 -5
View File
@@ -24,13 +24,13 @@ export function Cities() {
<div class="aggregator pageWorkspace">
<div class="pageHeading"><div><span class="eyebrow">Historical observations</span><h1>City analysis</h1><p>Choose stations and a calculation, then compare the resulting values.</p></div><div class="selectionCount">{getCities().size} cities selected</div></div>
<div class="container">
<aside class="column panel cityPanel">
<aside class="column cityPanel workspaceRail">
<h2>1. Choose cities</h2>
<SelectCity getCities={getCities} setCities={setCities} />
</aside>
<aside class="column panel queryPanel">
<aside class="column queryPanel workspaceToolbar">
<h2>2. Configure query</h2>
<div>
<div class="queryTimeRange">
<SelectTimeRange
getStart={getStart}
setStart={setStart}
@@ -38,8 +38,8 @@ export function Cities() {
setEnd={setEnd}
/>
</div>
<div>
<div>
<div class="queryOptions">
<div class="queryField">
<h4>Select field</h4>
<SelectField getField={getField} setField={setField} />
</div>
+5 -3
View File
@@ -36,12 +36,14 @@ export function Faktiska() {
return <div class="mapGraphicsPage pageWorkspace">
<div class="pageHeading"><div><span class="eyebrow">Daily newsroom production</span><h1>Faktiskā</h1><p>Create the actual-weather map with temperatures and weather symbols.</p></div><div class="selectionCount">{cities().size} cities selected</div></div>
<div class="graphicsSetup">
<aside class="panel cityPanel"><h2>1. Choose cities</h2><SelectCity getCities={cities} setCities={setCities}/></aside>
<aside class="cityPanel workspaceRail"><h2>1. Choose cities</h2><SelectCity getCities={cities} setCities={setCities}/></aside>
<section class="graphicsMain">
<div class="panel productionOptions"><h2>2. Choose output</h2>
<div class="productionToolbar">
<div class="productionOptions"><h2>2. Choose output</h2>
<div class="choiceGroup"><span>Output</span><div><button classList={{active: resolution() === "1920x1080"}} onClick={() => setResolution("1920x1080")}>1920 × 1080</button><button classList={{active: resolution() === "3840x1440"}} onClick={() => setResolution("3840x1440")}>3840 × 1440</button><label class="windChoice"><input type="checkbox" checked={wind()} onChange={e => setWind(e.target.checked)}/> Wind layout</label></div></div>
</div>
<div class="panel dataOptions"><h2>3. Retrieve temperatures</h2><div class="dataOptionsGrid"><SelectTimeRange getStart={start} setStart={setStart} getEnd={end} setEnd={setEnd}/><label><span>Temperature field</span><select value={field()} onChange={e => setField(e.target.value)}><option value="tempMax">Maximum temperature</option><option value="tempMin">Minimum temperature</option><option value="tempAvg">Average temperature</option></select></label><label><span>Calculation</span><select value={key()} onChange={e => setKey(e.target.value)}><option value="max">Maximum</option><option value="min">Minimum</option><option value="avg">Average</option></select></label></div><button class="primary" disabled={!cities().size} onClick={() => setTrigger(Date.now())}>Load map data</button></div>
<div class="dataOptions"><h2>3. Retrieve temperatures</h2><div class="dataOptionsGrid"><SelectTimeRange getStart={start} setStart={setStart} getEnd={end} setEnd={setEnd}/><label><span>Temperature field</span><select value={field()} onChange={e => setField(e.target.value)}><option value="tempMax">Maximum temperature</option><option value="tempMin">Minimum temperature</option><option value="tempAvg">Average temperature</option></select></label><label><span>Calculation</span><select value={key()} onChange={e => setKey(e.target.value)}><option value="max">Maximum</option><option value="min">Minimum</option><option value="avg">Average</option></select></label><button class="primary loadMapButton" disabled={!cities().size} onClick={() => setTrigger(Date.now())}>Load map data</button></div></div>
</div>
<Show when={result.loading}><LoadingSpinner text="Loading map data"/></Show>
<Show when={result.error}><div class="emptyState"><strong>Could not load map data</strong><span>{result.error.message}</span></div></Show>
<Show when={result() && isQueryResult(result())}><section class="mapProduction"><h2>4. Add conditions and export</h2><MapView type={mapType()} data={cityData} mode="faktiska"/></section></Show>
@@ -1,2 +1,34 @@
.graphicsSetup{display:grid;grid-template-columns:240px minmax(0,1fr);gap:18px;align-items:start}.graphicsMain{display:grid;gap:18px}.productionOptions,.dataOptions{padding:20px}.choiceGroup{display:grid;grid-template-columns:130px 1fr;gap:16px;align-items:start;margin-top:16px}.choiceGroup>span,.dataOptions label>span{display:block;font-size:12px;font-weight:700}.choiceGroup>div{display:flex;flex-wrap:wrap;gap:9px}.choiceGroup button{min-width:150px;text-align:left}.choiceGroup button small{display:block;margin-top:3px;color:var(--text-muted);font-family:Inter,sans-serif}.choiceGroup button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong)}.windChoice{display:flex;align-items:center;gap:7px;padding:9px 12px}.dataOptionsGrid{display:grid;grid-template-columns:minmax(250px,1.4fr) 1fr 1fr;gap:18px;align-items:end;margin-bottom:16px}.dataOptions select{width:100%;margin-top:6px}.mapProduction>h2{margin:0 0 10px}.symbolEditor{min-width:0}.symbolEditorHeading{display:flex;align-items:start;justify-content:space-between;gap:12px}.symbolPalette{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0 16px}.citySymbols{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.citySymbolRow{display:grid;grid-template-columns:1fr 52px 54px;gap:6px;align-items:center;padding:7px;border:1px solid var(--border);border-radius:9px}.assignedSymbol{font-family:'Daira by LTV grafika';font-size:28px;padding:2px}.clearSymbol{padding:5px;font-size:11px}@media(max-width:900px){.graphicsSetup{grid-template-columns:1fr}.dataOptionsGrid{grid-template-columns:1fr}.choiceGroup{grid-template-columns:1fr}}
.symbolEditorHeading{gap:16px}.bulkSymbol{display:flex;align-items:center;gap:8px}.selectedPreview,.currentSymbol{display:flex;align-items:center;justify-content:center;font-family:'Daira by LTV grafika';font-size:30px}.selectedPreview{width:42px;height:42px;border-radius:9px;background:#17212d;color:#fff}.symbolPalette{display:grid;grid-template-columns:repeat(auto-fill,46px);gap:8px;margin:14px 0 22px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--surface-soft)}.assignmentHeading{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.assignmentHeading span{color:var(--text-muted);font-size:12px}.citySymbols{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}.citySymbolRow{grid-template-columns:minmax(90px,1fr) 48px 100px 36px;gap:8px;min-height:62px;padding:8px 10px;border-radius:12px;background:#fff}.cityName{overflow:hidden;text-overflow:ellipsis;font-weight:700}.currentSymbol{width:44px;height:44px;border:1px solid #d7dfe8;border-radius:9px;background:#f6f8fa}.currentSymbol.empty{font-family:Inter,sans-serif;font-size:18px;color:#98a2b3}.assignAction{padding:7px 9px;font-size:12px}.clearSymbol{width:36px;padding:0;font-size:20px;color:var(--text-muted)}@media(max-width:900px){.symbolEditorHeading{flex-direction:column}.citySymbols{grid-template-columns:1fr}}
/* Production layout: a compact control band above one uninterrupted workspace. */
.graphicsSetup{grid-template-columns:230px minmax(0,1fr);column-gap:28px}
.mapGraphicsPage .workspaceRail{
padding:4px 24px 10px 0;
border-right:1px solid rgba(90,126,166,.2);
}
.mapGraphicsPage .workspaceRail h2{margin:0 0 18px;font-size:16px}
.graphicsMain{gap:16px}
.productionToolbar{
padding:2px 0 20px;
border-bottom:1px solid rgba(90,126,166,.22);
}
.productionOptions,.dataOptions{padding:0}
.productionOptions h2,.dataOptions h2{margin:0;font-size:18px}
.productionOptions{display:grid;grid-template-columns:210px minmax(0,1fr);gap:24px;align-items:center}
.productionOptions .choiceGroup{margin:0}
.dataOptions{display:grid;grid-template-columns:210px minmax(0,1fr);gap:24px;align-items:center;margin-top:18px;padding-top:18px;border-top:1px solid rgba(90,126,166,.14)}
.dataOptionsGrid{grid-template-columns:minmax(300px,1.35fr) minmax(180px,1fr) minmax(160px,.9fr) auto;gap:16px;margin:0;align-items:end}
.loadMapButton{white-space:nowrap;align-self:end}
.mapProduction>h2{font-size:22px}
@media(max-width:1200px){
.productionOptions,.dataOptions{grid-template-columns:1fr;gap:12px}
.dataOptionsGrid{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:900px){
.graphicsSetup{grid-template-columns:1fr}
.mapGraphicsPage .workspaceRail{padding-right:0;border-right:0;border-bottom:1px solid rgba(90,126,166,.2)}
.dataOptionsGrid{grid-template-columns:1fr}
.loadMapButton{justify-self:start}
}