step 9.4: done
This commit is contained in:
+26
-33
@@ -4,8 +4,8 @@
|
|||||||
> If this file disagrees with the code, trust the code and fix this file.
|
> If this file disagrees with the code, trust the code and fix this file.
|
||||||
|
|
||||||
## 1. Where we are
|
## 1. Where we are
|
||||||
- **Last completed step:** 9.3 (error pages, shared toast system, loading states) — TICKED in STATUS.md
|
- **Last completed step:** 9.4 (Mobile-responsive pass) — TICKED in STATUS.md
|
||||||
- **Next step:** Phase 9 — 9.4 (Mobile-responsive pass per UI_GUIDE)
|
- **Next step:** Phase 9 — 9.5 (Final full run of "Definition of playable" from NOTES.md §7)
|
||||||
- **Updated:** 2026-10-21
|
- **Updated:** 2026-10-21
|
||||||
- **Was the last session cut off mid-task?** No
|
- **Was the last session cut off mid-task?** No
|
||||||
|
|
||||||
@@ -18,26 +18,23 @@
|
|||||||
| `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear |
|
| `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear |
|
||||||
|
|
||||||
## 3. What exists now (files that matter)
|
## 3. What exists now (files that matter)
|
||||||
- `public/404.html` — Custom 404 error page (branding: "Page Not Found", Emberfall theme).
|
- `public/game.html` — Dashboard with 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), tabbed action area.
|
||||||
- `public/500.html` — Custom 500 error page (branding: "Something went wrong", retry button).
|
- `public/js/game.js` — hunt UI + inventory UI + expedition UI + mobile nav init (DOMContentLoaded); uses shared toast.js.
|
||||||
- `public/js/toast.js` — Shared toast notification system (window.showToast, 3 types: error/info/success).
|
- `public/css/main.css` — All styles including 3-col desktop grid, mobile bottom nav (< 700px), stacked panels on mobile, spinner, toast, button loading states.
|
||||||
- `public/game.html` — Dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition).
|
- `public/404.html` — Custom 404 error page.
|
||||||
- `public/js/game.js` — hunt UI + inventory UI + expedition UI (uses shared toast.js).
|
- `public/500.html` — Custom 500 error page.
|
||||||
- `public/shop.html` — Dedicated shop page.
|
- `public/js/toast.js` — Shared toast notification system.
|
||||||
- `public/js/shop.js` — buy/sell handlers (uses shared toast.js).
|
- `public/shop.html` / `public/js/shop.js` — Shop page (buy/sell).
|
||||||
- `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation (all include toast.js).
|
- `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation.
|
||||||
- `public/profile.html` — Public hero profile page.
|
- `public/profile.html` / `public/js/profile.js` — Public hero profile page.
|
||||||
- `public/js/profile.js` — Fetches GET /api/heroes/:name, renders profile.
|
|
||||||
- `public/js/auth.js` — Shared API wrapper + showError.
|
- `public/js/auth.js` — Shared API wrapper + showError.
|
||||||
- `public/css/main.css` — All styles including spinner, toast, button loading states, error page shared CSS.
|
- `scripts/seed-dev.js` — Creates demo account + hero + starter gear.
|
||||||
- `scripts/seed-dev.js` — Creates demo account + hero + starter gear (idempotent, skips in production).
|
|
||||||
- `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving.
|
- `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving.
|
||||||
- `src/services/*` — All game services (auth, character, hunt, inventory, shop, expedition, leaderboard, profile).
|
- `src/services/*` — All game services.
|
||||||
- `src/routes/*` — All route handlers.
|
- `src/routes/*` — All route handlers.
|
||||||
- `src/game/*` — Pure game logic (combat, stats, energy, leveling, rewards).
|
- `src/game/*` — Pure game logic.
|
||||||
- `tests/integration/error-pages.test.js` — 3 tests for 404/500 page serving.
|
- `tests/integration/*.test.js` — 90 integration tests.
|
||||||
- `tests/integration/*.test.js` — 90 integration tests across auth, combat, hunt, inventory, shop, expeditions, profile, leaderboard, error-pages.
|
- `tests/unit/*.test.js` — 85 unit tests.
|
||||||
- `tests/unit/*.test.js` — 85 unit tests for game logic.
|
|
||||||
|
|
||||||
## 4. Half-finished / fragile things
|
## 4. Half-finished / fragile things
|
||||||
- No leaderboard UI yet (Phase 8 API done but no UI).
|
- No leaderboard UI yet (Phase 8 API done but no UI).
|
||||||
@@ -45,21 +42,19 @@
|
|||||||
- No restart-persistence test for sessions.
|
- No restart-persistence test for sessions.
|
||||||
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`.
|
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`.
|
||||||
- Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
|
- Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
|
||||||
- Starter gear (GAME_DESIGN.md §Items) documented but not auto-created on character creation.
|
- Starter gear documented but not auto-created on character creation.
|
||||||
- `GET /api/zones` documented in API.md but not implemented (Issue #2).
|
- `GET /api/zones` documented in API.md but not implemented (Issue #2).
|
||||||
|
- Mobile nav uses emoji icons (⚔️ 🏕️ 🎒) — verify they render acceptably on all platforms.
|
||||||
|
|
||||||
## 5. Decisions & deviations from the docs
|
## 5. Decisions & deviations from the docs
|
||||||
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
|
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
|
||||||
|
- **D-014:** Mobile bottom nav uses emoji icons (⚔️ Hunt / 🏕️ Trip / 🎒 Items) instead of full text labels to save horizontal space on narrow screens.
|
||||||
|
- **D-015:** Desktop tabs hidden via CSS (`display: none`) below 700px; mobile nav shown via CSS; JS `syncMobileNav()` keeps active state in sync.
|
||||||
- Zod query param parsing uses `zod.coerce.number()` because Express query params are strings.
|
- Zod query param parsing uses `zod.coerce.number()` because Express query params are strings.
|
||||||
- Inventory response uses `itemId` (item.id) and `inventoryId` (inventory row id) to avoid confusion.
|
- Inventory response uses `itemId` (item.id) and `inventoryId` (inventory row id) to avoid confusion.
|
||||||
- Consumable effect parsing: regex `^(heal|energy):(\d+)$` on items.effect field.
|
- Consumable effect parsing: regex `^(heal|energy):(\d+)$` on items.effect field.
|
||||||
- Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
|
- Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
|
||||||
- **D-008:** Shop materials with buy_price=0 sell at `floor(default_rate * character_level / item_min_level)`.
|
- D-008..D-013 from previous steps.
|
||||||
- **D-009:** `sellItem` checks equipped_slot before material type.
|
|
||||||
- **D-010:** `startExpedition` checks active-expedition before energy.
|
|
||||||
- **D-011:** `computeEnergy` uses `now()` from utils/time.
|
|
||||||
- **D-012:** General `/api` rate limiter at 120 req/min per IP (SECURITY.md spec), 1000 in test mode.
|
|
||||||
- **D-013:** All `innerHTML` with dynamic data removed from game.js and profile.js — use createElement/textContent.
|
|
||||||
|
|
||||||
## 6. Gotchas learned
|
## 6. Gotchas learned
|
||||||
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
|
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
|
||||||
@@ -81,10 +76,8 @@
|
|||||||
- Express `sendFile` rejects `../` path segments — use `path.resolve()` + `path.join()` for absolute paths.
|
- Express `sendFile` rejects `../` path segments — use `path.resolve()` + `path.join()` for absolute paths.
|
||||||
- `express.static` sends its own 404 responses; catch-all must be placed BEFORE static middleware.
|
- `express.static` sends its own 404 responses; catch-all must be placed BEFORE static middleware.
|
||||||
|
|
||||||
## 7. Next-step briefing (9.4)
|
## 7. Next-step briefing (9.5)
|
||||||
- **Goal:** Mobile-responsive pass per UI_GUIDE (< 700px: single column, tabs become bottom nav).
|
- **Goal:** Final full run of "Definition of playable" from NOTES.md §7.
|
||||||
- **Docs:** UI_GUIDE §game.html layout (Mobile section).
|
- Walk the complete flow: register → log in → create hero → see stats/energy → hunt → gain XP/gold/loot → level up → equip items → buy/sell in shop → start and claim expedition → see leaderboard → log out → log back in (verify persistence).
|
||||||
- Current CSS has some `@media (max-width: 600px)` rules but needs comprehensive mobile review.
|
- Document any gaps or broken steps.
|
||||||
- Should: verify all pages render correctly on mobile viewports.
|
- No code changes expected unless a critical blocker is found.
|
||||||
- Should: tabs → bottom nav on mobile (per UI_GUIDE).
|
|
||||||
- Should NOT introduce new dependencies — use vanilla HTML/CSS/JS.
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
- **Phase:** 9
|
- **Phase:** 9
|
||||||
- **Step:** 9.4
|
- **Step:** 9.5
|
||||||
- **Blocked?** No
|
- **Blocked?** No
|
||||||
- **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.3
|
- **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.4
|
||||||
|
|
||||||
## Project Health
|
## Project Health
|
||||||
| Item | State |
|
| Item | State |
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
- [x] **9.2** Add `npm run seed:dev` creating a demo account and hero.
|
- [x] **9.2** Add `npm run seed:dev` creating a demo account and hero.
|
||||||
*Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production.
|
*Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production.
|
||||||
- [x] **9.3** Global error pages / friendly UI error toasts. Loading states.
|
- [x] **9.3** Global error pages / friendly UI error toasts. Loading states.
|
||||||
- [ ] **9.4** Mobile-responsive pass (see UI_GUIDE).
|
- [x] **9.4** Mobile-responsive pass (see UI_GUIDE).
|
||||||
- [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below.
|
- [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below.
|
||||||
- [ ] **9.6** Update README with real run instructions.
|
- [ ] **9.6** Update README with real run instructions.
|
||||||
|
|
||||||
@@ -153,11 +153,12 @@ Severity: `blocker` / `major` / `minor` / `cosmetic`. Status: `open` / `fixing`
|
|||||||
## Test Summary
|
## Test Summary
|
||||||
| Suite | Passing | Failing | Last run |
|
| Suite | Passing | Failing | Last run |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| unit | 85 | 0 | 2026-10-21 step 8.3 |
|
| unit | 85 | 0 | 2026-10-21 step 9.4 |
|
||||||
| integration | 87 | 0 | 2026-10-21 step 8.3 |
|
| integration | 90 | 0 | 2026-10-21 step 9.4 |
|
||||||
|
|
||||||
## Session Log
|
## Session Log
|
||||||
_Format:_ `YYYY-MM-DD | step | what changed | result`
|
_Format:_ `YYYY-MM-DD | step | what changed | result`
|
||||||
|
- 2026-10-21 | 9.4 | Mobile-responsive pass: game.html 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), desktop tabs hidden < 700px, game-container padding for nav clearance, mobile stat-row stacking, inventory container stacks < 600px; 175/175 tests pass
|
||||||
- 2026-10-21 | 8.3 | Created public profile UI: `public/profile.html` + `public/js/profile.js` (fetches GET /api/heroes/:name via ?name=, displays hero name/class/level/gold/stats/equipment; 404 handling, URL-safe names); 5 integration tests | 172/172 tests pass
|
- 2026-10-21 | 8.3 | Created public profile UI: `public/profile.html` + `public/js/profile.js` (fetches GET /api/heroes/:name via ?name=, displays hero name/class/level/gold/stats/equipment; 404 handling, URL-safe names); 5 integration tests | 172/172 tests pass
|
||||||
- 2026-10-21 | 7.3 | Added expedition UI: GET /api/expeditions/defs endpoint (expeditionService.listDefinitions + expeditions route), CSS styles for expedition elements, game.html expedition tab, game.js expedition UI (renderExpeditionDefs, renderActiveExpedition, handleStartExpedition, handleClaimExpedition, startExpeditionSync), 2 integration tests for defs endpoint | 153/153 tests pass
|
- 2026-10-21 | 7.3 | Added expedition UI: GET /api/expeditions/defs endpoint (expeditionService.listDefinitions + expeditions route), CSS styles for expedition elements, game.html expedition tab, game.js expedition UI (renderExpeditionDefs, renderActiveExpedition, handleStartExpedition, handleClaimExpedition, startExpeditionSync), 2 integration tests for defs endpoint | 153/153 tests pass
|
||||||
- 2026-10-21 | 7.2 | Created `src/services/expeditionService.js` (start, getActive, claim), `src/routes/expeditions.js` (3 endpoints), integrated into app.js; 11 integration tests covering start, active, claim, error paths, double-claim, cross-user rejection, time-based finish | 151/151 tests pass
|
- 2026-10-21 | 7.2 | Created `src/services/expeditionService.js` (start, getActive, claim), `src/routes/expeditions.js` (3 endpoints), integrated into app.js; 11 integration tests covering start, active, claim, error paths, double-claim, cross-user rejection, time-based finish | 151/151 tests pass
|
||||||
|
|||||||
+67
-4
@@ -76,9 +76,10 @@ button:hover { background: #c73650; }
|
|||||||
/* Game dashboard layout */
|
/* Game dashboard layout */
|
||||||
.game-container {
|
.game-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
padding-bottom: 5rem; /* space for mobile bottom nav */
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
@@ -99,12 +100,24 @@ button:hover { background: #c73650; }
|
|||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1.5fr 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 700px) {
|
||||||
.main-content { grid-template-columns: 1fr; }
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.game-container {
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide desktop tabs on mobile */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
#action-panel > .tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
@@ -332,6 +345,19 @@ button:hover { background: #c73650; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Stat rows (mobile: stack name/value) ===== */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.stat-row > .label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.stat-row > .label:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Equipment panel ===== */
|
/* ===== Equipment panel ===== */
|
||||||
.equipment-panel h4 {
|
.equipment-panel h4 {
|
||||||
color: var(--accent, #e2822b);
|
color: var(--accent, #e2822b);
|
||||||
@@ -661,6 +687,43 @@ button:hover { background: #c73650; }
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Mobile bottom navigation ===== */
|
||||||
|
.mobile-bottom-nav {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #16213e;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.mobile-bottom-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-bottom-nav .tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--muted, #a59a8a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-bottom-nav .tab.active {
|
||||||
|
color: var(--accent, #e2822b);
|
||||||
|
border-bottom-color: var(--accent, #e2822b);
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Profile page ===== */
|
/* ===== Profile page ===== */
|
||||||
.profile-header {
|
.profile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+12
-5
@@ -17,9 +17,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content (3-col on desktop, stacks on mobile) -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<!-- Stats panel -->
|
<!-- Left column: Stats -->
|
||||||
<section class="panel stats-panel" id="stats-panel">
|
<section class="panel stats-panel" id="stats-panel">
|
||||||
<h3>Your Hero</h3>
|
<h3>Your Hero</h3>
|
||||||
<div id="hero-loading" class="loading">Loading...</div>
|
<div id="hero-loading" class="loading">Loading...</div>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Action panel (tabs: Hunt / Inventory) -->
|
<!-- Center column: Tabs + action area -->
|
||||||
<section class="panel action-panel" id="action-panel">
|
<section class="panel action-panel" id="action-panel">
|
||||||
<div class="tabs" role="tablist">
|
<div class="tabs" role="tablist">
|
||||||
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
|
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">Hunt</button>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Combat log panel -->
|
<!-- Right column: Combat log -->
|
||||||
<section class="panel combat-panel" id="combat-panel">
|
<section class="panel combat-panel" id="combat-panel">
|
||||||
<h3>Combat Log</h3>
|
<h3>Combat Log</h3>
|
||||||
<div id="combat-log-list" class="combat-log-list">
|
<div id="combat-log-list" class="combat-log-list">
|
||||||
@@ -112,12 +112,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Result panel -->
|
<!-- Result panel (spans full width on mobile via grid-column) -->
|
||||||
<section class="panel result-panel" id="result-panel" style="display:none;">
|
<section class="panel result-panel" id="result-panel" style="display:none;">
|
||||||
<h3 id="result-title"></h3>
|
<h3 id="result-title"></h3>
|
||||||
<div id="result-body"></div>
|
<div id="result-body"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Mobile bottom navigation (replaces tabs on small screens) -->
|
||||||
|
<nav class="mobile-bottom-nav" role="tablist">
|
||||||
|
<button class="tab active" data-tab="hunt" role="tab" aria-selected="true">⚔️ Hunt</button>
|
||||||
|
<button class="tab" data-tab="expedition" role="tab" aria-selected="false">🏕️ Trip</button>
|
||||||
|
<button class="tab" data-tab="inventory" role="tab" aria-selected="false">🎒 Items</button>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
|
|||||||
@@ -21,6 +21,36 @@ function switchTab(tab) {
|
|||||||
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
|
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync mobile bottom nav active state with desktop tabs.
|
||||||
|
*/
|
||||||
|
function syncMobileNav() {
|
||||||
|
const mobileNav = document.querySelector('.mobile-bottom-nav');
|
||||||
|
if (!mobileNav) return;
|
||||||
|
mobileNav.querySelectorAll('.tab').forEach((t) => {
|
||||||
|
const isActive = t.dataset.tab === currentTab;
|
||||||
|
t.classList.toggle('active', isActive);
|
||||||
|
t.setAttribute('aria-selected', isActive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach mobile bottom-nav click handlers.
|
||||||
|
*/
|
||||||
|
function initMobileNav() {
|
||||||
|
const mobileNav = document.querySelector('.mobile-bottom-nav');
|
||||||
|
if (!mobileNav) return;
|
||||||
|
mobileNav.querySelectorAll('.tab').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
switchTab(btn.dataset.tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initMobileNav();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh energy state from the server at a fixed interval.
|
* Refresh energy state from the server at a fixed interval.
|
||||||
* Keeps client-side countdown in sync with the authoritative server.
|
* Keeps client-side countdown in sync with the authoritative server.
|
||||||
|
|||||||
Reference in New Issue
Block a user