From ce63734746ca1c222ecfe78c455465ff998cc892 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:39:43 +0900 Subject: [PATCH 01/40] docs: design repository hardening work --- .../2026-07-13-repository-hardening-design.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-repository-hardening-design.md diff --git a/docs/superpowers/specs/2026-07-13-repository-hardening-design.md b/docs/superpowers/specs/2026-07-13-repository-hardening-design.md new file mode 100644 index 00000000..7878bc9c --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-repository-hardening-design.md @@ -0,0 +1,174 @@ +# Repository Hardening Design + +## Goal + +Bring `ground.codes` from a working production project to a consistently +verifiable, documented, and maintainable state. The work covers production +smoke reliability, current documentation, formatting and CI enforcement, +source-file size compliance, stale branch cleanup, full-data verification, and +desktop/mobile visual QA. + +## Current State + +- `main` is deployed and its latest CI run is green. +- Production smoke succeeds most of the time, but issue #65 records intermittent + failures when `/metrics` does not contain samples from earlier smoke requests. +- The API runs on Cloudflare Workers. Request metrics are stored in module-local + memory and therefore describe one Worker isolate, not a globally aggregated + service view. +- The 180-language structural and automated quality gates are complete, while + native-speaker review remains ongoing maintenance. +- The root README still describes the older 11-language and 60-language target. +- Prettier reports 67 nonconforming files, and CI does not run format, lint, or + build gates. +- Thirty-five checked-in TypeScript, TSX, or MJS files exceed the stated + 450-line policy. One large landmark-label module is generated output; the + remaining oversized files require either decomposition or a narrowly defined + generated/fixture exception. +- Fourteen remote branches are unmerged and stale. Their functionality appears + to have been superseded, but deletion requires branch-by-branch evidence. +- The local disk cannot currently hold dependencies plus the complete 8.35 GiB + generated geoint checkout, so development uses sparse checkout temporarily. + +## Architecture and Work Breakdown + +### 1. Isolate-aware operational metrics + +`GET /metrics` will explicitly return `scope: "worker-isolate"`. The endpoint +continues to expose lightweight counters without adding a database or Durable +Object dependency. This keeps the endpoint cheap and accurately describes the +data it already provides. + +Production smoke will verify: + +- the endpoint is reachable and identifies `api-ground-codes`; +- `scope` is `worker-isolate`; +- request totals and route maps have the documented object/number shapes; and +- any routes that are present have valid non-negative counters. + +Production smoke will not require its preceding requests to appear in the same +isolate's counters. Local API tests remain responsible for proving that a single +app instance records `/readyz`, `/v1/encode`, and `/v1/search` correctly. This +separates distributed availability monitoring from per-process instrumentation. + +The change will be developed test-first by replacing the route-presence helper +contract with a metrics-snapshot validation contract. The old production +failure fixture must fail before implementation and pass afterward. + +### 2. Documentation and quality gates + +The root and API READMEs will describe: + +- 180 structurally complete and automated-stable language sets; +- ongoing native-speaker review as a non-structural quality program; +- Earth, Moon, and Mars support; +- Cloudflare Workers plus Supabase/PostGIS as production architecture; and +- Worker-isolate semantics for the lightweight metrics endpoint. + +Language documents will use one definition: `stable` means the automated score +and regression gates pass; it does not claim native-speaker certification. + +The root package will add non-mutating `format:check` and `code:size-check` +commands. CI will run format, size, lint, type, test, and build gates. The API +build will declare its real output so Turbo no longer emits the missing-output +warning. + +The existing 67 formatting violations will be fixed in one mechanical commit, +separate from behavioral changes. + +### 3. Source-file size policy + +The 450-line policy will be machine-enforced for maintained source files. + +Generated data modules and large review fixtures may be exempt only when all of +the following are true: + +- the file is reproducible from a checked-in generator or is explicitly a test + fixture/review record; +- the file contains or is paired with a clear generated/fixture declaration; +- the exception is listed with a reason in the size-check configuration; and +- the generator or consuming code remains subject to the 450-line limit. + +Maintained production modules and generator scripts will be split by cohesive +responsibility. Public exports and runtime behavior must remain compatible. +Tests may be split by scenario without changing assertions. The checker itself +will have tests proving that an oversized maintained file fails, a normal file +passes, and only declared generated/fixture exceptions are accepted. + +Because the existing debt spans several independent areas, decomposition will +be done in small commits grouped by subsystem: metrics/API, web maps, core +encoding, codebook generators/audits, and test suites. + +### 4. Repository and storage hygiene + +Each stale branch will be recorded in a branch audit with: + +- branch name and tip SHA; +- unique commits; +- the `main` files or commits that supersede its behavior; and +- a delete/retain decision. + +Only branches with positive supersession evidence will be deleted. The audit +remains in the repository so deleted work can be traced by SHA. No force pushes +or history rewrites are allowed. + +Disk cleanup is limited to reproducible artifacts and caches: `node_modules`, +Turbo/Next/build outputs, and package-manager caches. User source files and +other projects are out of scope. Once sufficient space exists, sparse checkout +will be disabled in the verification worktree and dependencies reinstalled. + +### 5. Verification, visual QA, and release + +The complete checkout must pass: + +1. `pnpm install --frozen-lockfile` +2. `pnpm format:check` +3. `pnpm code:size-check` +4. `pnpm lint` +5. `pnpm check-types` +6. `pnpm scripts:test` +7. `pnpm language:audit` +8. `pnpm --filter ground-codes test` +9. `pnpm --filter ground-codes test:standalone` +10. `pnpm --filter api-ground-codes test` +11. `pnpm --filter web test` +12. `pnpm build` +13. the browser smoke suite + +Visual QA will inspect production-equivalent desktop and mobile widths for +Earth, Moon, and Mars. It will verify the initial view, landmark labels, grid, +marker selection, compass behavior, attribution, encoding panel, and control +overlap. Screenshots will be retained as workflow or local test artifacts. + +Changes will be pushed through a pull request. After CI is green, the branch is +merged, relevant deployments complete, and production smoke succeeds. Issue +#65 is closed only after the new smoke semantics are deployed and consecutive +scheduled or manually triggered runs pass. + +## Error Handling and Rollback + +- Metrics validation reports the exact invalid field instead of a generic route + absence message. +- A failed CI or visual check blocks merge; no bypass is used. +- Branch deletion occurs after the implementation PR is merged and after the + audit is committed. +- Deleted branches remain recoverable by the recorded tip SHA. +- If a full checkout cannot be completed after safe cache cleanup, verification + continues in GitHub Actions while the local disk limitation is reported; the + goal is not marked complete until authoritative full-data CI and visual + evidence are available. + +## Success Criteria + +- Production smoke no longer assumes cross-isolate metric aggregation and issue + #65 has verifiable post-deploy green runs. +- README and language/API documentation match the 180-language production + architecture and use a consistent stability definition. +- Format, size, lint, type, test, and build gates are enforced in CI. +- Every maintained source file satisfies the 450-line limit; every remaining + oversized checked-in file is an explicitly justified generated artifact or + fixture. +- All safely superseded remote branches are deleted and the decisions are + auditable. +- Full-data automated verification and desktop/mobile planetary visual QA pass. +- The merged production revision is healthy on Web, API, and Grok Spiral. From 8a51602783c9ce08a7aec396ba377103a3da4cdf Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:42:40 +0900 Subject: [PATCH 02/40] docs: plan metrics docs and CI hardening --- .../plans/2026-07-13-metrics-docs-ci.md | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-13-metrics-docs-ci.md diff --git a/docs/superpowers/plans/2026-07-13-metrics-docs-ci.md b/docs/superpowers/plans/2026-07-13-metrics-docs-ci.md new file mode 100644 index 00000000..87727f4c --- /dev/null +++ b/docs/superpowers/plans/2026-07-13-metrics-docs-ci.md @@ -0,0 +1,370 @@ +# Metrics, Documentation, and CI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove Cloudflare isolate-related production smoke flakiness, align public documentation with the deployed 180-language architecture, and enforce format/lint/build checks in CI. + +**Architecture:** Keep lightweight request metrics local to each Worker isolate and make that scope explicit. Production smoke validates the snapshot contract instead of assuming cross-request isolate affinity. Repository-level scripts expose non-mutating quality gates that CI runs before expensive data and browser tests. + +**Tech Stack:** TypeScript, Node.js test runner, Bun test, Elysia, pnpm, Turborepo, GitHub Actions, Prettier. + +--- + +### Task 1: Define the isolate-aware metrics snapshot contract + +**Files:** + +- Modify: `scripts/production-smoke.test.mjs` +- Modify: `scripts/production-smoke-helpers.mjs` + +- [ ] **Step 1: Replace the route-affinity test with failing snapshot tests** + +Update the helper import to use `validateMetricsSnapshot`, then add these tests: + +```js +test("accepts an empty Worker-isolate metrics snapshot", () => { + assert.deepEqual( + validateMetricsSnapshot({ + service: "api-ground-codes", + scope: "worker-isolate", + uptimeSeconds: 0, + requests: { total: 0, avgMs: 0, byPath: {}, routes: {} }, + }), + [], + ); +}); + +test("reports invalid metrics snapshot fields", () => { + assert.deepEqual( + validateMetricsSnapshot({ + service: "api-ground-codes", + scope: "global", + uptimeSeconds: -1, + requests: { total: -1, avgMs: "fast", byPath: [], routes: null }, + }), + [ + 'scope must be "worker-isolate"', + "uptimeSeconds must be a non-negative number", + "requests.total must be a non-negative number", + "requests.avgMs must be a non-negative number", + "requests.byPath must be an object", + "requests.routes must be an object", + ], + ); +}); +``` + +- [ ] **Step 2: Run the helper test and verify RED** + +Run: `node --test scripts/production-smoke.test.mjs` + +Expected: FAIL because `validateMetricsSnapshot` is not exported. + +- [ ] **Step 3: Implement the snapshot validator** + +Replace `getMissingMetricRoutes` with a `validateMetricsSnapshot(metrics)` export. It returns an array of exact error strings and uses a local `isRecord` helper: + +```js +const isRecord = (value) => + value !== null && typeof value === "object" && !Array.isArray(value); + +const isNonNegativeNumber = (value) => + typeof value === "number" && Number.isFinite(value) && value >= 0; + +export const validateMetricsSnapshot = (metrics) => { + const errors = []; + if (metrics?.scope !== "worker-isolate") { + errors.push('scope must be "worker-isolate"'); + } + if (!isNonNegativeNumber(metrics?.uptimeSeconds)) { + errors.push("uptimeSeconds must be a non-negative number"); + } + if (!isNonNegativeNumber(metrics?.requests?.total)) { + errors.push("requests.total must be a non-negative number"); + } + if (!isNonNegativeNumber(metrics?.requests?.avgMs)) { + errors.push("requests.avgMs must be a non-negative number"); + } + if (!isRecord(metrics?.requests?.byPath)) { + errors.push("requests.byPath must be an object"); + } + if (!isRecord(metrics?.requests?.routes)) { + errors.push("requests.routes must be an object"); + } + return errors; +}; +``` + +- [ ] **Step 4: Run the helper test and verify GREEN** + +Run: `node --test scripts/production-smoke.test.mjs` + +Expected: all production smoke helper tests pass. + +- [ ] **Step 5: Commit the contract helper** + +```bash +git add scripts/production-smoke.test.mjs scripts/production-smoke-helpers.mjs +git commit -m "test(smoke): define isolate metrics contract" +``` + +### Task 2: Expose and consume isolate scope + +**Files:** + +- Modify: `apps/api-ground-codes/src/app.test.ts` +- Modify: `apps/api-ground-codes/src/endpoints/metrics.ts` +- Modify: `scripts/production-smoke.mjs` + +- [ ] **Step 1: Add a failing API contract assertion** + +In `serves lightweight operational metrics`, assert: + +```ts +expect(body.scope).toBe("worker-isolate"); +``` + +- [ ] **Step 2: Run the focused API test and verify RED** + +Run: `pnpm --filter api-ground-codes test -- --test-name-pattern "lightweight operational metrics"` + +Expected: FAIL because the response does not have `scope`. + +- [ ] **Step 3: Return the scope from `/metrics`** + +Add the field next to `service`: + +```ts +return { + service: "api-ground-codes", + scope: "worker-isolate", + startedAt: requestMetrics.startedAt, + // existing fields +}; +``` + +- [ ] **Step 4: Replace the smoke route-affinity assertion** + +Import `validateMetricsSnapshot`, rename the smoke check to `API metrics snapshot`, and validate the fetched payload: + +```js +await smoke.check("API metrics snapshot", async () => { + const metrics = JSON.parse(await fetchText(`${apiBaseUrl}/metrics`)); + assert(metrics.service === "api-ground-codes", "unexpected metrics service"); + const errors = validateMetricsSnapshot(metrics); + assert(errors.length === 0, `invalid metrics snapshot: ${errors.join("; ")}`); +}); +``` + +- [ ] **Step 5: Run focused and helper tests** + +Run: + +```bash +pnpm --filter api-ground-codes test -- --test-name-pattern "lightweight operational metrics" +node --test scripts/production-smoke.test.mjs +``` + +Expected: both commands pass. + +- [ ] **Step 6: Commit the runtime change** + +```bash +git add apps/api-ground-codes/src/app.test.ts apps/api-ground-codes/src/endpoints/metrics.ts scripts/production-smoke.mjs +git commit -m "fix(smoke): respect Worker isolate metrics" +``` + +### Task 3: Align public documentation + +**Files:** + +- Modify: `README.md` +- Modify: `apps/api-ground-codes/README.md` +- Modify: `docs/language-expansion-180.md` +- Modify: `docs/language-native-review-backlog.md` +- Modify: `apps/api-ground-codes/src/endpoints/docs.ts` +- Modify: `apps/api-ground-codes/src/app.test.ts` + +- [ ] **Step 1: Add failing documentation assertions** + +Extend the API docs test with: + +```ts +expect(firstPartyDocs).toContain("Worker-isolate metrics"); +expect(firstPartyDocs).toContain("180 automated-stable language sets"); +``` + +- [ ] **Step 2: Run the API docs test and verify RED** + +Run: `pnpm --filter api-ground-codes test -- --test-name-pattern "public API documentation"` + +Expected: FAIL because the phrases are absent. + +- [ ] **Step 3: Update API documentation content** + +In the monitoring section, state that `/metrics` contains Worker-isolate counters and is not a globally aggregated request history. In the language section, state that 180 sets satisfy automated structural and quality gates while native-speaker review is ongoing. + +- [ ] **Step 4: Update repository documentation consistently** + +Replace the root README's 11-language/60-language statements with 180 automated-stable language sets. Update the comparison and technical-detail sections to use the same wording. In `language-expansion-180.md`, replace the final paragraph with: + +```md +All 180 languages meet the repository's automated `stable` gate. Here, +`stable` means structural, regression, and minimum-score checks pass; it does +not mean native-speaker certification. Native-speaker review remains ongoing +maintenance tracked in `docs/language-native-review-backlog.md`. +``` + +- [ ] **Step 5: Run documentation tests and format checks for touched files** + +Run: + +```bash +pnpm --filter api-ground-codes test -- --test-name-pattern "public API documentation" +pnpm exec prettier --check README.md apps/api-ground-codes/README.md docs/language-expansion-180.md docs/language-native-review-backlog.md apps/api-ground-codes/src/endpoints/docs.ts apps/api-ground-codes/src/app.test.ts +``` + +Expected: both commands pass. + +- [ ] **Step 6: Commit documentation alignment** + +```bash +git add README.md apps/api-ground-codes/README.md docs/language-expansion-180.md docs/language-native-review-backlog.md apps/api-ground-codes/src/endpoints/docs.ts apps/api-ground-codes/src/app.test.ts +git commit -m "docs: align production and language status" +``` + +### Task 4: Add format, lint, and build quality gates + +**Files:** + +- Modify: `package.json` +- Modify: `.github/workflows/ci.yml` +- Modify: `scripts/qa-workflows.test.mjs` + +- [ ] **Step 1: Add failing workflow assertions** + +Add a test that reads `.github/workflows/ci.yml` and asserts the commands are present: + +```js +test("enforces format lint and build gates in CI", () => { + const workflow = readFileSync( + resolve(repoRoot, ".github/workflows/ci.yml"), + "utf8", + ); + for (const command of ["pnpm format:check", "pnpm lint", "pnpm build"]) { + assert.match(workflow, new RegExp(command.replaceAll(":", "\\:"))); + } +}); +``` + +- [ ] **Step 2: Run the workflow test and verify RED** + +Run: `node --test scripts/qa-workflows.test.mjs` + +Expected: FAIL because the commands are not in CI. + +- [ ] **Step 3: Add package scripts** + +Add: + +```json +"format:check": "prettier --check \"**/*.{ts,tsx,md}\"" +``` + +- [ ] **Step 4: Add format, lint, and build CI steps** + +Place format immediately after dependency installation, lint before type checking, and build after unit tests. + +- [ ] **Step 5: Run workflow and JSON validation** + +Run: + +```bash +node --test scripts/qa-workflows.test.mjs +pnpm exec prettier --check package.json .github/workflows/ci.yml scripts/qa-workflows.test.mjs +``` + +Expected: both commands pass. + +- [ ] **Step 6: Commit the CI gate wiring** + +```bash +git add package.json .github/workflows/ci.yml scripts/qa-workflows.test.mjs +git commit -m "ci: enforce repository quality gates" +``` + +### Task 5: Mechanically format the repository + +**Files:** + +- Modify: all files reported by `pnpm format:check` + +- [ ] **Step 1: Capture the failing format gate** + +Run: `pnpm format:check` + +Expected: FAIL and report the existing 67 files. + +- [ ] **Step 2: Apply only Prettier's mechanical rewrite** + +Run: `pnpm format` + +Expected: Prettier rewrites supported TypeScript, TSX, and Markdown files. + +- [ ] **Step 3: Verify formatting and inspect scope** + +Run: + +```bash +pnpm format:check +git diff --stat +git diff --check +``` + +Expected: format check passes; diff contains formatting-only changes with no whitespace errors. + +- [ ] **Step 4: Re-run behavior gates affected by formatted source** + +Run: + +```bash +pnpm lint +pnpm check-types +pnpm --filter web test +node --test scripts/production-smoke.test.mjs scripts/qa-workflows.test.mjs +``` + +Expected: all commands pass. + +- [ ] **Step 5: Commit the mechanical change separately** + +```bash +git add --all +git commit -m "style: format repository sources" +``` + +### Task 6: Verify this plan's completed slice + +**Files:** + +- Verify only + +- [ ] **Step 1: Run the slice verification suite** + +Run: + +```bash +pnpm format:check +pnpm lint +pnpm check-types +pnpm --filter web test +node --test scripts/production-smoke.test.mjs scripts/qa-workflows.test.mjs +pnpm build +git status --short +``` + +Expected: every command exits zero and the worktree is clean. + +- [ ] **Step 2: Record the plan checkpoint** + +Update this plan's checkboxes to reflect executed steps and commit the plan-state update together with the next implementation checkpoint rather than creating a documentation-only completion commit. From 61f3f53d124214f1bfc47405d50c99e36f6c8351 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:43:48 +0900 Subject: [PATCH 03/40] test(smoke): define isolate metrics contract --- scripts/production-smoke-helpers.mjs | 48 ++++++++++++++++++++-------- scripts/production-smoke.test.mjs | 39 +++++++++++++++------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/scripts/production-smoke-helpers.mjs b/scripts/production-smoke-helpers.mjs index 55d52ed0..4dd60ff9 100644 --- a/scripts/production-smoke-helpers.mjs +++ b/scripts/production-smoke-helpers.mjs @@ -9,11 +9,13 @@ export const createSmokeRecorder = ({ logger = console } = {}) => { const startedAt = performance.now(); try { await run(); - const durationMs = Math.round((performance.now() - startedAt) * 100) / 100; + const durationMs = + Math.round((performance.now() - startedAt) * 100) / 100; results.push({ name, ok: true, durationMs }); logger.log?.(`ok ${name} ${durationMs}ms`); } catch (error) { - const durationMs = Math.round((performance.now() - startedAt) * 100) / 100; + const durationMs = + Math.round((performance.now() - startedAt) * 100) / 100; const message = error instanceof Error ? error.message : String(error); failures.push(`${name}: ${message}`); results.push({ name, ok: false, durationMs }); @@ -31,12 +33,7 @@ const sleep = (delayMs) => export const fetchWithRetry = async ( url, - { - fetchImpl = fetch, - retries = 2, - retryDelayMs = 500, - ...init - } = {}, + { fetchImpl = fetch, retries = 2, retryDelayMs = 500, ...init } = {}, ) => { let lastError; @@ -53,12 +50,35 @@ export const fetchWithRetry = async ( throw lastError; }; -export const getMissingMetricRoutes = (metrics, requiredRoutes) => { - const routes = metrics?.requests?.routes ?? {}; - return requiredRoutes.filter((route) => { - const count = routes[route]?.count; - return typeof count !== "number" || count < 1; - }); +const isRecord = (value) => + value !== null && typeof value === "object" && !Array.isArray(value); + +const isNonNegativeNumber = (value) => + typeof value === "number" && Number.isFinite(value) && value >= 0; + +export const validateMetricsSnapshot = (metrics) => { + const errors = []; + + if (metrics?.scope !== "worker-isolate") { + errors.push('scope must be "worker-isolate"'); + } + if (!isNonNegativeNumber(metrics?.uptimeSeconds)) { + errors.push("uptimeSeconds must be a non-negative number"); + } + if (!isNonNegativeNumber(metrics?.requests?.total)) { + errors.push("requests.total must be a non-negative number"); + } + if (!isNonNegativeNumber(metrics?.requests?.avgMs)) { + errors.push("requests.avgMs must be a non-negative number"); + } + if (!isRecord(metrics?.requests?.byPath)) { + errors.push("requests.byPath must be an object"); + } + if (!isRecord(metrics?.requests?.routes)) { + errors.push("requests.routes must be an object"); + } + + return errors; }; export const formatSmokeSummary = (results) => diff --git a/scripts/production-smoke.test.mjs b/scripts/production-smoke.test.mjs index 482989d8..d87a9c97 100644 --- a/scripts/production-smoke.test.mjs +++ b/scripts/production-smoke.test.mjs @@ -5,7 +5,7 @@ import { createSmokeRecorder, fetchWithRetry, formatGitHubStepSummary, - getMissingMetricRoutes, + validateMetricsSnapshot, } from "./production-smoke-helpers.mjs"; describe("production smoke monitoring helpers", () => { @@ -29,19 +29,34 @@ describe("production smoke monitoring helpers", () => { assert.ok(recorder.results.every((result) => result.durationMs >= 0)); }); - test("identifies required API routes missing from metrics", () => { - const metrics = { - requests: { - routes: { - "/readyz": { count: 1 }, - "/v1/encode": { count: 2 }, - }, - }, - }; + test("accepts an empty Worker-isolate metrics snapshot", () => { + assert.deepEqual( + validateMetricsSnapshot({ + service: "api-ground-codes", + scope: "worker-isolate", + uptimeSeconds: 0, + requests: { total: 0, avgMs: 0, byPath: {}, routes: {} }, + }), + [], + ); + }); + test("reports invalid metrics snapshot fields", () => { assert.deepEqual( - getMissingMetricRoutes(metrics, ["/readyz", "/v1/encode", "/v1/search"]), - ["/v1/search"], + validateMetricsSnapshot({ + service: "api-ground-codes", + scope: "global", + uptimeSeconds: -1, + requests: { total: -1, avgMs: "fast", byPath: [], routes: null }, + }), + [ + 'scope must be "worker-isolate"', + "uptimeSeconds must be a non-negative number", + "requests.total must be a non-negative number", + "requests.avgMs must be a non-negative number", + "requests.byPath must be an object", + "requests.routes must be an object", + ], ); }); From 51efbcb3e78e5fd77acbb5bb0e5dc865afe0cbd2 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:44:27 +0900 Subject: [PATCH 04/40] fix(smoke): respect Worker isolate metrics --- apps/api-ground-codes/src/app.test.ts | 1 + apps/api-ground-codes/src/endpoints/metrics.ts | 6 ++++-- scripts/production-smoke.mjs | 15 ++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/api-ground-codes/src/app.test.ts b/apps/api-ground-codes/src/app.test.ts index ad5b851d..81486502 100644 --- a/apps/api-ground-codes/src/app.test.ts +++ b/apps/api-ground-codes/src/app.test.ts @@ -71,6 +71,7 @@ describe("Ground Codes API contract", () => { expect(response.status).toBe(200); const body = await response.json(); expect(body.service).toBe("api-ground-codes"); + expect(body.scope).toBe("worker-isolate"); expect(body.requests.total).toBeGreaterThan(0); }); diff --git a/apps/api-ground-codes/src/endpoints/metrics.ts b/apps/api-ground-codes/src/endpoints/metrics.ts index 4924cdf8..566c5a10 100644 --- a/apps/api-ground-codes/src/endpoints/metrics.ts +++ b/apps/api-ground-codes/src/endpoints/metrics.ts @@ -104,6 +104,7 @@ export const metricsEndpoint = new Elysia() return { service: "api-ground-codes", + scope: "worker-isolate", startedAt: requestMetrics.startedAt, uptimeSeconds: Math.round( (Date.now() - Date.parse(requestMetrics.startedAt)) / 1000, @@ -113,8 +114,9 @@ export const metricsEndpoint = new Elysia() avgMs: requestMetrics.total === 0 ? 0 - : Math.round((requestMetrics.totalMs / requestMetrics.total) * 100) / - 100, + : Math.round( + (requestMetrics.totalMs / requestMetrics.total) * 100, + ) / 100, byPath: requestMetrics.byPath, routes: serializeRoutes(), }, diff --git a/scripts/production-smoke.mjs b/scripts/production-smoke.mjs index d64d3c05..8c65584e 100644 --- a/scripts/production-smoke.mjs +++ b/scripts/production-smoke.mjs @@ -5,7 +5,7 @@ import { fetchWithRetry, formatGitHubStepSummary, formatSmokeSummary, - getMissingMetricRoutes, + validateMetricsSnapshot, } from "./production-smoke-helpers.mjs"; const apiBaseUrl = ( @@ -1063,17 +1063,14 @@ await smoke.check("Unsupported route is not a server error", async () => { ); }); -await smoke.check("API route metrics cover smoke paths", async () => { +await smoke.check("API metrics snapshot", async () => { const metrics = JSON.parse(await fetchText(`${apiBaseUrl}/metrics`)); - const missingRoutes = getMissingMetricRoutes(metrics, [ - "/readyz", - "/v1/encode", - "/v1/search", - ]); assert( - missingRoutes.length === 0, - `metrics missing route samples: ${missingRoutes.join(", ")}`, + metrics.service === "api-ground-codes", + `unexpected metrics service: ${metrics.service}`, ); + const errors = validateMetricsSnapshot(metrics); + assert(errors.length === 0, `invalid metrics snapshot: ${errors.join("; ")}`); }); await smoke.check("Web robots", async () => { From 9fedff662a6c2a0f0d0acd3a574b882e681c9ed5 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:45:58 +0900 Subject: [PATCH 05/40] docs: align production and language status --- README.md | 14 ++++----- apps/api-ground-codes/README.md | 10 +++++-- apps/api-ground-codes/src/app.test.ts | 2 ++ apps/api-ground-codes/src/endpoints/docs.ts | 32 +++++++++------------ docs/language-expansion-180.md | 8 +++--- docs/language-native-review-backlog.md | 5 +++- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c4eed5ca..a215f75c 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,16 @@ And Ground Codes is a **multi-planetary addressing system** 🪐 that provides a - 🧠 **Simple and Memorable**: Just three words to identify any location precisely - 🌎 **Global Coverage**: Works anywhere in the world with a unique address - 🔓 **Open Source**: MIT licensed and fully transparent implementation -- 🌐 **Multilingual Support**: Currently available in English, Korean, Chinese, Japanese, Spanish, French, German, Portuguese, Indonesian, Thai, and Vietnamese, with plans to expand to 60 languages +- 🌐 **Multilingual Support**: 180 language sets include codebooks, localized UI copy, and Earth, Moon, and Mars region labels. All 180 pass the automated structural, regression, and minimum-score gates; native-speaker review remains ongoing maintenance. - 🎯 **Variable Precision**: Offers three levels of precision (3m, 30cm, and 3cm) to suit different use cases ## 📊 Comparison with Similar Services -| Service | Format | License | Precision | Global Usage | Multilingual Support | -| ----------------------- | ----------------------- | --------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| 🌍 **Ground Codes** | Yongsan-Happiness-Smile | ✅ MIT License (Free) | 1. 3 meters (standard)
2. 30cm
3. 3cm | ✅ Yes | 🌐 English, Korean, Chinese, Japanese, Spanish, French, German, Portuguese, Indonesian, Thai, Vietnamese (expanding to 60 languages) | -| 🔍 **Google Plus Code** | HX2F+J8 | ⚠️ No License (Free) | 3.5 meters | ⚠️ Limited (requires 4 additional characters for global use, e.g., **8Q94HX2F+J8**) | 🇬🇧 English only | -| 🔤 **What 3 Words** | ///teacher.awaken.days | 💰 Proprietary (Paid) | 3 meters | ✅ Yes | 🌐 60 languages | +| Service | Format | License | Precision | Global Usage | Multilingual Support | +| ----------------------- | ----------------------- | --------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| 🌍 **Ground Codes** | Yongsan-Happiness-Smile | ✅ MIT License (Free) | 1. 3 meters (standard)
2. 30cm
3. 3cm | ✅ Yes | 🌐 180 automated-stable language sets; native-speaker review continues | +| 🔍 **Google Plus Code** | HX2F+J8 | ⚠️ No License (Free) | 3.5 meters | ⚠️ Limited (requires 4 additional characters for global use, e.g., **8Q94HX2F+J8**) | 🇬🇧 English only | +| 🔤 **What 3 Words** | ///teacher.awaken.days | 💰 Proprietary (Paid) | 3 meters | ✅ Yes | 🌐 60 languages | ## 💪 Advantages Over Existing Services @@ -78,7 +78,7 @@ API usage is limited to 600 requests per minute per IP. For higher volume needs, - 🇰🇷 Korean word set: 5,630 words (AI-generated dataset) - 🇨🇳 Chinese word set: 5,140 words (AI-generated dataset) - 🇯🇵 Japanese word set: 5,000 words (frequency-guided hiragana dataset) -- 🌐 Region labels: English, Korean, Chinese, Japanese, Spanish, French, German, Portuguese, Indonesian, Thai, and Vietnamese labels are available for Earth, Moon, and Mars datasets +- 🌐 Language coverage: 180 codebooks and localized UI/region-label sets are available for Earth, Moon, and Mars datasets - 🌎 Region names: 210,000 unique global locations with populations of 500+ (GeoNames data, commercially usable) - 🔐 Special solutions: - **Region 1**: ✈️ Optimized for airports/logistics with country codes and airport codes (e.g., NYC-491AD, SSN-TA14C) diff --git a/apps/api-ground-codes/README.md b/apps/api-ground-codes/README.md index 31a415f4..30e48d9c 100644 --- a/apps/api-ground-codes/README.md +++ b/apps/api-ground-codes/README.md @@ -14,7 +14,7 @@ API Ground.codes is a RESTful API service built with Elysia.js and Bun that prov - 🔄 **Encode Coordinates**: Convert latitude and longitude to memorable ground codes - 🔍 **Decode Ground Codes**: Convert ground codes back to geographic coordinates - 🌎 **Region Information**: Get information about specific regions -- 🌐 **Multilingual Support**: Support for multiple languages (English, Korean, Chinese, Japanese, Spanish, French, German, Portuguese, Indonesian, Thai) +- 🌐 **Multilingual Support**: 180 automated-stable codebook, UI, and Earth/Moon/Mars region-label sets; native-speaker review remains ongoing maintenance - 🌕 **Planetary Bodies**: Encode Earth, Moon, and Mars coordinates with body-specific labels - 🎯 **Customizable Precision**: Adjust the precision of encoded locations - 📝 **Swagger Documentation**: Interactive API documentation @@ -180,6 +180,12 @@ web app, and API route metrics. It records per-check response times for `/readyz writes the timing table to the GitHub run summary. Add a `MOSHI_WEBHOOK_TOKEN` repository secret to send a webhook alert when the smoke workflow fails. +`GET /metrics` exposes Worker-isolate metrics. The counters describe the +Cloudflare Worker isolate that handled the metrics request and are not a +globally aggregated request history. Production smoke validates the endpoint's +schema and numeric invariants without assuming earlier requests reached the same +isolate. + ## 🔌 API Endpoints ### 🧩 Core Endpoints @@ -315,7 +321,7 @@ Response: The API supports various configuration options: - 🏙️ **Region Level**: Choose between city names (level 2) or airport codes (level 1) -- 🌐 **Language**: Select from supported languages (English, Korean, Chinese, Japanese, Spanish, French, German, Portuguese, Indonesian, Thai) +- 🌐 **Language**: Select from 180 automated-stable language sets - 📏 **Precision**: Adjust the precision of encoded locations in meters - 🪐 **Body**: Select `earth`, `moon`, or `mars` for coordinate conversion and labels - 🔐 **CORS**: Set `CORS_ALLOWED_ORIGINS` as a comma-separated production allowlist diff --git a/apps/api-ground-codes/src/app.test.ts b/apps/api-ground-codes/src/app.test.ts index 81486502..1f4c4742 100644 --- a/apps/api-ground-codes/src/app.test.ts +++ b/apps/api-ground-codes/src/app.test.ts @@ -93,6 +93,8 @@ describe("Ground Codes API contract", () => { expect(firstPartyDocs).toContain("https://api.ground.codes/v1/encode"); expect(firstPartyDocs).toContain("https://ground.codes/moon/"); expect(firstPartyDocs).toContain("curl https://api.ground.codes/metrics"); + expect(firstPartyDocs).toContain("Worker-isolate metrics"); + expect(firstPartyDocs).toContain("180 automated-stable language sets"); expect(firstPartyDocs).toContain("biasLat"); expect(firstPartyDocs).toContain("Copy-ready Examples"); expect(firstPartyDocs).toContain("https://api.ground.codes/v1/decode"); diff --git a/apps/api-ground-codes/src/endpoints/docs.ts b/apps/api-ground-codes/src/endpoints/docs.ts index 8cc7ee2f..7b28b342 100644 --- a/apps/api-ground-codes/src/endpoints/docs.ts +++ b/apps/api-ground-codes/src/endpoints/docs.ts @@ -91,7 +91,8 @@ const docsHtml = `

Languages

-

Ground Codes supports english, korean, chinese, japanese, spanish, french, german, portuguese, indonesian, thai, vietnamese, hindi, arabic, russian, swahili, filipino, hausa, bengali, urdu, amharic, burmese, khmer, nepali, somali, pashto, and lingala for codebooks and localized region labels.

+

Ground Codes ships 180 automated-stable language sets with codebooks, UI copy, and localized Earth, Moon, and Mars region labels. Automated-stable means the structural, regression, and minimum-score gates pass; native-speaker review remains ongoing maintenance.

+

Examples include english, korean, chinese, japanese, spanish, french, german, portuguese, indonesian, thai, vietnamese, hindi, arabic, russian, swahili, filipino, hausa, bengali, urdu, amharic, burmese, khmer, nepali, somali, pashto, and lingala.

Share URL Rules

@@ -138,10 +139,11 @@ const docsHtml = `

Operational Endpoints

+

Worker-isolate metrics are lightweight diagnostics for the Cloudflare Worker instance that serves the request. They are not a globally aggregated request history.

  • /healthz liveness
  • /readyz deployment readiness
  • -
  • /metrics request counts and latency
  • +
  • /metrics Worker-isolate request counts and latency
curl https://api.ground.codes/metrics
@@ -160,27 +162,19 @@ const docsHtml = ` `; const serveDocs = ({ set }: { set: { headers: Record } }) => { - set.headers["cache-control"] = "public, max-age=300"; - set.headers["content-type"] = "text/html; charset=utf-8"; - return docsHtml; + set.headers["cache-control"] = "public, max-age=300"; + set.headers["content-type"] = "text/html; charset=utf-8"; + return docsHtml; }; export const docsEndpoint = new Elysia() - .get( - "/", - serveDocs, - { - detail: { - hide: true, - }, + .get("/", serveDocs, { + detail: { + hide: true, }, - ) - .get( - "/docs", - serveDocs, - { + }) + .get("/docs", serveDocs, { detail: { hide: true, }, - }, -); + }); diff --git a/docs/language-expansion-180.md b/docs/language-expansion-180.md index dbad1cc7..84d7aa6b 100644 --- a/docs/language-expansion-180.md +++ b/docs/language-expansion-180.md @@ -109,7 +109,7 @@ Current verified baseline: - Codebook count, uniqueness, URL-safety, and English-mirroring checks covered by `language-support-completeness.test.mjs`. -The remaining quality risk is linguistic rather than structural: many languages -still need native-speaker review before they should be marked `stable` in -`packages/codebook/LANGUAGE_QUALITY.md`. Track that work in -`docs/language-native-review-backlog.md`. +All 180 languages meet the repository's automated `stable` gate. Here, +`stable` means structural, regression, and minimum-score checks pass; it does +not mean native-speaker certification. Native-speaker review remains ongoing +maintenance tracked in `docs/language-native-review-backlog.md`. diff --git a/docs/language-native-review-backlog.md b/docs/language-native-review-backlog.md index bbeb4bf4..d92569a6 100644 --- a/docs/language-native-review-backlog.md +++ b/docs/language-native-review-backlog.md @@ -10,6 +10,9 @@ Current automated status: - `active cleanup`: 0 - `pnpm language:audit`: required before shipping language-support changes +In this repository, `stable` means structural, regression, and minimum-score +checks pass. It does not mean native-speaker certification. + Useful report commands: ```sh @@ -36,7 +39,7 @@ checked public surfaces. | Category | Count | Primary action | | ----------------------------- | ----: | ------------------------------------------------------------------------------ | | Generated fallback vocabulary | 0 | No active row currently names unresolved generated fallback vocabulary. | -| Native lexical review | 0 | No active row currently names unresolved native lexical review. | +| Native lexical review | 0 | No active row currently names unresolved native lexical review. | | Script-specific review | 0 | No current language rows are explicitly marked for script-specific cleanup. | | Standalone expansion | 0 | No current language rows are explicitly marked for standalone expansion. | | Region terminology | 0 | No current language rows are explicitly marked for region terminology cleanup. | From 7a7184699b26ea0cc016efe1f98423a6c3671d00 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:46:41 +0900 Subject: [PATCH 06/40] ci: enforce repository quality gates --- .github/workflows/ci.yml | 9 +++++++++ package.json | 1 + scripts/qa-workflows.test.mjs | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9eaeac8..9c31f29a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check formatting + run: pnpm format:check + - name: Check API runtime pins run: pnpm runtime:check-pins @@ -45,6 +48,9 @@ jobs: - name: Report URL label data run: pnpm data:report-labels + - name: Lint + run: pnpm lint + - name: Type check run: pnpm check-types @@ -60,6 +66,9 @@ jobs: - name: Test web client run: pnpm --filter web test + - name: Build + run: pnpm build + - name: Install Playwright browser run: pnpm --filter web exec playwright install --with-deps chromium diff --git a/package.json b/package.json index faa35e16..7b188210 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dev": "turbo run dev", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,md}\"", "check-types": "turbo run check-types", "production:smoke": "node scripts/production-smoke.mjs", "data:audit-labels": "pnpm --filter ground-codes test:data-audit && node --test scripts/region-label-quality.test.mjs", diff --git a/scripts/qa-workflows.test.mjs b/scripts/qa-workflows.test.mjs index a9eadcd7..bb46b0a5 100644 --- a/scripts/qa-workflows.test.mjs +++ b/scripts/qa-workflows.test.mjs @@ -6,6 +6,14 @@ const readText = (path) => readFileSync(new URL(path, import.meta.url), "utf8"); const readJson = (path) => JSON.parse(readText(path)); describe("QA workflow split", () => { + test("enforces format lint and build gates in CI", () => { + const ciWorkflow = readText("../.github/workflows/ci.yml"); + + for (const command of ["pnpm format:check", "pnpm lint", "pnpm build"]) { + assert.match(ciWorkflow, new RegExp(command)); + } + }); + test("keeps default CI on the lightweight browser smoke script", () => { const webPackage = readJson("../apps/web/package.json"); const ciWorkflow = readText("../.github/workflows/ci.yml"); From e5d7f79cc1ff24d4adad92b7bb5e37739d57b1ea Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Mon, 13 Jul 2026 08:47:29 +0900 Subject: [PATCH 07/40] style: format repository sources --- apps/api-ground-codes/src/endpoints/cors.ts | 6 +- .../api-ground-codes/src/endpoints/swagger.ts | 74 +- .../src/endpoints/v1/v1-endpoints.ts | 10 +- apps/api-ground-codes/src/index.ts | 2 +- apps/grok-spiral/lib/grok-spiral.ts | 132 +- apps/web/README.md | 8 - apps/web/components/disable-zoom.tsx | 6 +- .../components/google-map/context/index.ts | 6 +- .../google-map/context/use-map-context.ts | 2 +- .../components/google-map/earth-3d-map.tsx | 5 +- .../google-map/hooks/use-air-quality.ts | 12 +- .../hooks/use-device-orientation.ts | 19 +- .../google-map/hooks/use-location-tracking.ts | 8 +- .../google-map/hooks/use-map-coordinates.ts | 2 +- apps/web/components/google-map/map-search.tsx | 23 +- .../google-map/place-details/helpers.ts | 81 +- .../google-map/place-details/index.tsx | 77 +- .../place-details/photo-gallery.tsx | 9 +- .../google-map/place-details/photo-modal.tsx | 2 +- .../google-map/place-details/use-copy.ts | 2 +- .../place-details/use-ground-code.ts | 2 +- .../place-details/use-place-details.ts | 9 +- .../google-map/selected-area-summary.ts | 7 +- apps/web/components/i18n-provider-wrapper.tsx | 6 +- apps/web/e2e/visual-qa.spec.ts | 9 +- apps/web/hooks/use-disable-zoom.ts | 4 +- apps/web/lib/code/share-url.test.ts | 4 +- apps/web/lib/code/share-url.ts | 7 +- apps/web/lib/grid-system/use-map-handlers.ts | 12 +- apps/web/lib/i18n/i18n-context.tsx | 2 +- packages/codebook/LANGUAGE_QUALITY.md | 364 ++--- .../chinese-review-2026-05-10.md | 480 +++--- .../english-review-2026-05-10.md | 322 ++-- .../korean/urgent-review-2026-05-10.md | 1442 ++++++++--------- packages/codebook/src/build-codebook.ts | 24 +- packages/codebook/src/generate-codebook.ts | 58 +- packages/codebook/src/refine-codebook.ts | 54 +- packages/geoint/README.md | 4 - .../create-japanese-region-labels.ts | 7 +- .../geoint/build-script/region-1-build.ts | 56 +- .../region-2-build-translation.ts | 96 +- .../geoint/build-script/region-2-build.ts | 36 +- .../region-2-create-pre-translation.ts | 24 +- .../region-2-create-translation.ts | 52 +- .../benchmarks/spiral-performance.md | 162 +- .../scripts/benchmark-spiral-scale.ts | 20 +- .../ground-codes/scripts/benchmark-spiral.ts | 4 +- .../scripts/check-spiral-bigint.ts | 2 +- .../scripts/compare-spiral-baseline.ts | 13 +- .../scripts/explore-lattice-count-research.ts | 38 +- .../scripts/explore-spiral-index.ts | 18 +- .../scripts/explore-spiral-math.ts | 7 +- .../scripts/generate-spiral-fixture.ts | 12 +- .../scripts/stress-spiral-edge-cases.ts | 6 +- packages/ground-codes/src/spiral.ts | 132 +- .../ground-codes/test/celestial-body.test.ts | 5 +- .../test/japanese-wordset.test.ts | 18 +- .../test/multilingual-codebook-review.test.ts | 5 +- .../test/region-3-dataset.test.ts | 10 +- .../ground-codes/test/spiral-fixture.test.ts | 14 +- packages/ui/src/components/grid-canvas.tsx | 18 +- packages/ui/src/components/spiral-viewer.tsx | 2 +- packages/ui/src/components/ui/scroll-area.tsx | 2 +- 63 files changed, 2112 insertions(+), 1943 deletions(-) diff --git a/apps/api-ground-codes/src/endpoints/cors.ts b/apps/api-ground-codes/src/endpoints/cors.ts index f50c5166..08db9f92 100644 --- a/apps/api-ground-codes/src/endpoints/cors.ts +++ b/apps/api-ground-codes/src/endpoints/cors.ts @@ -6,10 +6,12 @@ export const getAllowedOriginsFromEnv = () => .map((origin) => origin.trim()) .filter(Boolean); -export const createCorsEndpoint = (allowedOrigins = getAllowedOriginsFromEnv()) => +export const createCorsEndpoint = ( + allowedOrigins = getAllowedOriginsFromEnv(), +) => new Elysia() .onAfterHandle(({ request, set }) => { - // * Only process CORS requests + // * Only process CORS requests if (request.method !== "OPTIONS") return; const allowHeader = set.headers["Access-Control-Allow-Headers"]; diff --git a/apps/api-ground-codes/src/endpoints/swagger.ts b/apps/api-ground-codes/src/endpoints/swagger.ts index fed88c27..0ab24621 100644 --- a/apps/api-ground-codes/src/endpoints/swagger.ts +++ b/apps/api-ground-codes/src/endpoints/swagger.ts @@ -54,7 +54,7 @@ export const swaggerEndpoint = swagger({ info: { title: "Ground Codes API Documentation", description: - "Production API documentation for Ground Codes. Use the versioned `/v1/*` endpoints for new integrations. Quick start: POST `/v1/encode` with `{ \"lat\": 37.566, \"lng\": 126.978, \"language\": \"english\", \"regionLevel\": 2 }`, then POST `/v1/search` with the returned code or share it as `https://ground.codes/{encoded-code}`. Earth share URLs are code-only; Moon and Mars use `/moon/{encoded-code}` and `/mars/{encoded-code}`.", + 'Production API documentation for Ground Codes. Use the versioned `/v1/*` endpoints for new integrations. Quick start: POST `/v1/encode` with `{ "lat": 37.566, "lng": 126.978, "language": "english", "regionLevel": 2 }`, then POST `/v1/search` with the returned code or share it as `https://ground.codes/{encoded-code}`. Earth share URLs are code-only; Moon and Mars use `/moon/{encoded-code}` and `/mars/{encoded-code}`.', version: "1.0.0", }, tags: [ @@ -116,59 +116,35 @@ export const openApiReferenceEndpoint = new Elysia() }, }, ) - .get( - "/openapi/json", - () => redirect("/openapi-json/json"), - { - detail: { - hide: true, - }, + .get("/openapi/json", () => redirect("/openapi-json/json"), { + detail: { + hide: true, }, - ); + }); export const swaggerRedirectEndpoint = new Elysia() - .get( - "/json", - () => redirect("/openapi-json/json"), - { - detail: { - hide: true, - }, + .get("/json", () => redirect("/openapi-json/json"), { + detail: { + hide: true, }, - ) - .get( - "/reference", - () => redirect("/openapi/"), - { - detail: { - hide: true, - }, + }) + .get("/reference", () => redirect("/openapi/"), { + detail: { + hide: true, }, - ) - .get( - "/swagger", - () => redirect("/openapi/"), - { - detail: { - hide: true, - }, + }) + .get("/swagger", () => redirect("/openapi/"), { + detail: { + hide: true, }, - ) - .get( - "/swagger/", - () => redirect("/openapi/"), - { - detail: { - hide: true, - }, + }) + .get("/swagger/", () => redirect("/openapi/"), { + detail: { + hide: true, }, - ) - .get( - "/swagger/json", - () => redirect("/openapi-json/json"), - { - detail: { - hide: true, - }, + }) + .get("/swagger/json", () => redirect("/openapi-json/json"), { + detail: { + hide: true, }, - ); + }); diff --git a/apps/api-ground-codes/src/endpoints/v1/v1-endpoints.ts b/apps/api-ground-codes/src/endpoints/v1/v1-endpoints.ts index e273b222..de9e89df 100644 --- a/apps/api-ground-codes/src/endpoints/v1/v1-endpoints.ts +++ b/apps/api-ground-codes/src/endpoints/v1/v1-endpoints.ts @@ -7,11 +7,11 @@ import { v1Search } from "./search"; const endpointGroup = () => new Elysia() - .use(v1Encode) - .use(v1Decode) - .use(v1Search) - .use(v1RegionAround) - .use(v1RegionInfo); + .use(v1Encode) + .use(v1Decode) + .use(v1Search) + .use(v1RegionAround) + .use(v1RegionInfo); export const v1Endpoints = new Elysia({ prefix: "/v1" }).use(endpointGroup()); diff --git a/apps/api-ground-codes/src/index.ts b/apps/api-ground-codes/src/index.ts index 552d9b4f..63b33463 100644 --- a/apps/api-ground-codes/src/index.ts +++ b/apps/api-ground-codes/src/index.ts @@ -10,6 +10,6 @@ void (async function () { const app = createApp(process.env.PORT ?? 3000); console.log( - `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}` + `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`, ); })(); diff --git a/apps/grok-spiral/lib/grok-spiral.ts b/apps/grok-spiral/lib/grok-spiral.ts index 7088c51f..0b836622 100644 --- a/apps/grok-spiral/lib/grok-spiral.ts +++ b/apps/grok-spiral/lib/grok-spiral.ts @@ -9,15 +9,17 @@ export function getNFromCoordinates(x: number, y: number): number | bigint; export function getNFromCoordinates(x: bigint, y: bigint): bigint; export function getNFromCoordinates( x: number | bigint, - y: number | bigint + y: number | bigint, ): number | bigint; export function getNFromCoordinates( x: number | bigint, - y: number | bigint + y: number | bigint, ): number | bigint { if (typeof x === "bigint" || typeof y === "bigint") { if (typeof x !== "bigint" || typeof y !== "bigint") { - throw new Error("x and y must both be bigint when using BigInt coordinates."); + throw new Error( + "x and y must both be bigint when using BigInt coordinates.", + ); } return getNFromBigIntCoordinates(x, y); } @@ -50,19 +52,16 @@ export function getNFromCoordinates( export function getCoordinates(n: number): { x: number; y: number }; export function getCoordinates(n: bigint): { x: bigint; y: bigint }; export function getCoordinates( - n: number | bigint + n: number | bigint, ): { x: number; y: number } | { x: bigint; y: bigint }; export function getCoordinates( - n: number | bigint + n: number | bigint, ): { x: number; y: number } | { x: bigint; y: bigint } { if (typeof n === "bigint") return getBigIntCoordinates(n); if (n <= 0) throw new Error("Invalid value for n."); if (n === 1) return { x: 0, y: 0 }; - if ( - Number.isInteger(n) && - n >= Number(BIGINT_COORDINATE_SEARCH_THRESHOLD) - ) { + if (Number.isInteger(n) && n >= Number(BIGINT_COORDINATE_SEARCH_THRESHOLD)) { const coordinates = getBigIntCoordinates(BigInt(n)); return { x: Number(coordinates.x), y: Number(coordinates.y) }; } @@ -213,7 +212,7 @@ function countLatticePointsByConvexHullFlat(m: number): number { !pointInCircle( currentX + stackX[stackLength - 1]!, currentY - stackY[stackLength - 1]!, - m + m, ) ) { stackLength--; @@ -261,7 +260,7 @@ function slopeOutNoDivision( slopeX: number, slopeY: number, x: number, - m: number + m: number, ): boolean { return slopeY * Math.sqrt(m - x * x) > slopeX * x; } @@ -282,7 +281,7 @@ function addSymmetricPoints(points: [number, number][], x: number, y: number) { [y, x], [y, -x], [-y, x], - [-y, -x] + [-y, -x], ); } } @@ -347,7 +346,7 @@ function angleHalf([, y]: [number, number]): 0 | 1 { function compareByAngleDescending( a: [number, number], - b: [number, number] + b: [number, number], ): number { const halfA = angleHalf(a); const halfB = angleHalf(b); @@ -438,13 +437,18 @@ function getBigIntCoordinates(n: bigint): { x: bigint; y: bigint } { let { low, high } = getBigIntSearchBoundsByInterpolation(n, count) ?? getBigIntSearchBounds(n, count); - ({ low, high } = refineBigIntSearchBoundsByInterpolation(n, low, high, count)); + ({ low, high } = refineBigIntSearchBoundsByInterpolation( + n, + low, + high, + count, + )); ({ low, high } = refineBigIntSearchBoundsBySecant(n, low, high, count)); const scannedCoordinates = tryResolveBigIntCoordinatesByShellScan( n, low, high, - count + count, ); if (scannedCoordinates) return scannedCoordinates; @@ -470,7 +474,7 @@ function tryResolveBigIntCoordinatesByShellScan( n: bigint, low: bigint, high: bigint, - count: BigIntCountFunction + count: BigIntCountFunction, ): { x: bigint; y: bigint } | undefined { const threshold = getBigIntShellScanSearchThreshold(high); if (low === 0n || high - low > threshold) { @@ -501,9 +505,10 @@ function refineBigIntSearchBoundsBySecant( n: bigint, low: bigint, high: bigint, - count: BigIntCountFunction + count: BigIntCountFunction, ): { low: bigint; high: bigint } { - if (high - low <= getBigIntShellScanSearchThreshold(high)) return { low, high }; + if (high - low <= getBigIntShellScanSearchThreshold(high)) + return { low, high }; let lowCount = low > 0n ? count(low - 1n) : 0n; let highCount = count(high); @@ -537,7 +542,7 @@ function refineBigIntSearchBoundsByInterpolation( n: bigint, low: bigint, high: bigint, - count: BigIntCountFunction + count: BigIntCountFunction, ): { low: bigint; high: bigint } { let current = (low + high) / 2n; @@ -588,7 +593,7 @@ function createLocalBigIntCount(): BigIntCountFunction { function getBigIntSearchBoundsByInterpolation( n: bigint, - count: BigIntCountFunction + count: BigIntCountFunction, ): { low: bigint; high: bigint } | undefined { const numericN = Number(n); if (!Number.isFinite(numericN)) return undefined; @@ -624,7 +629,7 @@ function getBigIntSearchBoundsByInterpolation( function getBigIntSearchBounds( n: bigint, - count: BigIntCountFunction = countBigIntLatticePoints + count: BigIntCountFunction = countBigIntLatticePoints, ): { low: bigint; high: bigint } { const numericN = Number(n); if (!Number.isFinite(numericN)) return { low: 0n, high: n }; @@ -636,10 +641,7 @@ function getBigIntSearchBounds( const low = approx > delta ? approx - delta : 0n; const high = approx + delta; - if ( - (low === 0n || count(low - 1n) < n) && - count(high) >= n - ) { + if ((low === 0n || count(low - 1n) < n) && count(high) >= n) { return { low, high }; } @@ -685,7 +687,7 @@ function countBigIntLatticePointsByConvexHull(m: bigint): bigint { if (sqrtForGuidedPath <= MAX_NUMBER_GUIDED_BIGINT_ROOT) { return countBigIntLatticePointsByNumberGuidedConvexHull( m, - Number(sqrtForGuidedPath) + Number(sqrtForGuidedPath), ); } @@ -726,7 +728,7 @@ function countBigIntLatticePointsByConvexHull(m: bigint): bigint { !bigIntPointInCircle( currentX + stackX[stackLength - 1]!, currentY - stackY[stackLength - 1]!, - m + m, ) ) { stackLength--; @@ -764,7 +766,7 @@ function countBigIntLatticePointsByConvexHull(m: bigint): bigint { function countBigIntLatticePointsByNumberGuidedConvexHull( m: bigint, - sqrt: number + sqrt: number, ): bigint { let count = 0n; const numericM = Number(m); @@ -776,9 +778,9 @@ function countBigIntLatticePointsByNumberGuidedConvexHull( const naiveThreshold = Math.min( Math.max( 1, - Math.floor(cbrt / BIGINT_CONVEX_HULL_NAIVE_THRESHOLD_DIVISOR) + 1 + Math.floor(cbrt / BIGINT_CONVEX_HULL_NAIVE_THRESHOLD_DIVISOR) + 1, ), - sqrt + sqrt, ); count += sumNumberGuidedYInCircleRange(1, naiveThreshold, m, numericM); @@ -794,8 +796,7 @@ function countBigIntLatticePointsByNumberGuidedConvexHull( const bigSlopeX = BigInt(slopeX); const bigSlopeY = BigInt(slopeY); let bigCurrentY = BigInt(currentY); - const areaCorrection = - (BigInt(slopeX - 1) * (bigSlopeY - 1n)) / 2n; + const areaCorrection = (BigInt(slopeX - 1) * (bigSlopeY - 1n)) / 2n; while (true) { const nextX = currentX + slopeX; @@ -815,7 +816,7 @@ function countBigIntLatticePointsByNumberGuidedConvexHull( currentX + stackX[stackLength - 1]!, currentY - stackY[stackLength - 1]!, m, - numericM + numericM, ) ) { stackLength--; @@ -855,7 +856,7 @@ function sumNumberGuidedYInCircleRange( start: number, end: number, m: bigint, - numericM: number + numericM: number, ): bigint { if (start > end) return 0n; @@ -864,7 +865,9 @@ function sumNumberGuidedYInCircleRange( let squareStep = BigInt(2 * start + 1); for (let x = start; x <= end; x++) { - total += BigInt(maxNumberGuidedYInCircleWithSquare(x, xSquared, m, numericM)); + total += BigInt( + maxNumberGuidedYInCircleWithSquare(x, xSquared, m, numericM), + ); xSquared += squareStep; squareStep += 2n; } @@ -875,7 +878,7 @@ function sumNumberGuidedYInCircleRange( function maxNumberGuidedYInCircle( x: number, m: bigint, - numericM: number + numericM: number, ): number { const bigX = BigInt(x); return maxNumberGuidedYInCircleWithSquare(x, bigX * bigX, m, numericM); @@ -885,7 +888,7 @@ function maxNumberGuidedYInCircleWithSquare( x: number, xSquared: bigint, m: bigint, - numericM: number + numericM: number, ): number { let y = Math.floor(Math.sqrt(Math.max(0, numericM - x * x))); let bigY = BigInt(y); @@ -910,7 +913,7 @@ function numberGuidedPointInCircle( x: number, y: number, m: bigint, - numericM?: number + numericM?: number, ): boolean { if (numericM !== undefined) { const delta = numericM - (x * x + y * y); @@ -927,14 +930,14 @@ function numberGuidedPointInCircle( function getNumberGuidedPointCheckTolerance( numericM: number, x: number, - y: number + y: number, ): number { if (numericM <= NUMBER_GUIDED_DYNAMIC_EPSILON_THRESHOLD) { return NUMBER_GUIDED_POINT_CHECK_EPSILON; } return ( - (Math.abs(numericM) + Math.abs(x * x) + Math.abs(y * y)) * + (Math.abs(numericM) + Math.abs(x * x) + Math.abs(y * y)) * Number.EPSILON * NUMBER_GUIDED_LARGE_POINT_CHECK_ULP_FACTOR + NUMBER_GUIDED_POINT_CHECK_EPSILON @@ -946,7 +949,7 @@ function numberGuidedSlopeOutSquared( slopeY: number, x: number, m: bigint, - numericM?: number + numericM?: number, ): boolean { if (numericM !== undefined) { const xSquared = x * x; @@ -971,7 +974,6 @@ function numberGuidedSlopeOutSquared( return bigSlopeYSquared * (m - bigXSquared) > bigSlopeXSquared * bigXSquared; } - function maxBigIntYInCircle(x: bigint, m: bigint): bigint { return integerSqrt(m - x * x); } @@ -984,7 +986,7 @@ function bigIntSlopeOutSquared( slopeX: bigint, slopeY: bigint, x: bigint, - m: bigint + m: bigint, ): boolean { return slopeY * slopeY * (m - x * x) > slopeX * slopeX * x * x; } @@ -1037,7 +1039,7 @@ function getBigIntShellIndexDirect(m: bigint, x: bigint, y: bigint): bigint { for (const [px, py] of getBigIntSymmetricPointCandidates( representativeX, - representativeY + representativeY, )) { if (px === x && py === y) found = true; if (compareBigIntByAngleDescending([px, py], [x, y]) < 0) count++; @@ -1084,8 +1086,8 @@ function getBigIntPointsByFactorization(m: bigint): [bigint, bigint][] { options.push( gaussianBigIntMultiply( gaussianBigIntPow(primeRep, k), - gaussianBigIntPow(conjugate, exponent - k) - ) + gaussianBigIntPow(conjugate, exponent - k), + ), ); } @@ -1151,7 +1153,7 @@ function getBigIntSymmetricPoints(m: bigint): [bigint, bigint][] { function addBigIntSymmetricPoints( points: [bigint, bigint][], x: bigint, - y: bigint + y: bigint, ) { if (x === 0n && y === 0n) { points.push([0n, 0n]); @@ -1168,18 +1170,30 @@ function addBigIntSymmetricPoints( [y, x], [y, -x], [-y, x], - [-y, -x] + [-y, -x], ); } } function getBigIntSymmetricPointCandidates( x: bigint, - y: bigint + y: bigint, ): [bigint, bigint][] { if (x === 0n && y === 0n) return [[0n, 0n]]; - if (x === 0n) return [[0n, y], [0n, -y], [y, 0n], [-y, 0n]]; - if (x === y) return [[x, y], [x, -y], [-x, y], [-x, -y]]; + if (x === 0n) + return [ + [0n, y], + [0n, -y], + [y, 0n], + [-y, 0n], + ]; + if (x === y) + return [ + [x, y], + [x, -y], + [-x, y], + [-x, -y], + ]; return [ [x, y], @@ -1195,7 +1209,7 @@ function getBigIntSymmetricPointCandidates( function compareBigIntByAngleDescending( a: [bigint, bigint], - b: [bigint, bigint] + b: [bigint, bigint], ): number { const halfA = a[1] >= 0n ? 0 : 1; const halfB = b[1] >= 0n ? 0 : 1; @@ -1236,7 +1250,9 @@ function factorBigInt(value: bigint): Array<[bigint, number]> { } addFactor(n); - return [...factors.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); + return [...factors.entries()].sort(([a], [b]) => + a < b ? -1 : a > b ? 1 : 0, + ); } function pollardRho(n: bigint): bigint { @@ -1361,7 +1377,7 @@ function tonelliShanks(n: bigint, prime: bigint): bigint { function gaussianBigIntPow( base: [bigint, bigint], - exponent: number + exponent: number, ): [bigint, bigint] { let result: [bigint, bigint] = [1n, 0n]; let power = base; @@ -1378,7 +1394,7 @@ function gaussianBigIntPow( function gaussianBigIntMultiply( a: [bigint, bigint], - b: [bigint, bigint] + b: [bigint, bigint], ): [bigint, bigint] { return [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]; } @@ -1415,7 +1431,8 @@ function absBigInt(value: bigint): bigint { } function integerSqrt(value: bigint): bigint { - if (value < 0n) throw new Error("Cannot calculate square root of negative bigint."); + if (value < 0n) + throw new Error("Cannot calculate square root of negative bigint."); if (value < 2n) return value; if (value <= MAX_FAST_BIGINT_ROOT) { @@ -1437,7 +1454,8 @@ function integerSqrt(value: bigint): bigint { } function integerCubeRoot(value: bigint): bigint { - if (value < 0n) throw new Error("Cannot calculate cube root of negative bigint."); + if (value < 0n) + throw new Error("Cannot calculate cube root of negative bigint."); if (value < 8n) return value >= 1n ? 1n : 0n; if (value <= MAX_FAST_BIGINT_ROOT) { diff --git a/apps/web/README.md b/apps/web/README.md index ecf3ac80..d896fb32 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -13,7 +13,6 @@ Ground.codes web application is an interactive map service utilizing the Google ## ✨ Features - **🗺️ Google Maps Integration** - - 🌓 Custom dark/light theme support - 📍 POI (Point of Interest) markers and place details display - 📱 User location tracking and display @@ -22,14 +21,12 @@ Ground.codes web application is an interactive map service utilizing the Google - 🔍 Place search functionality - **📏 Grid System** - - 🧩 Grid display on the map - 🌐 Body-aware grid sizing for Earth, Moon, and Mars - 👆 Grid cell click event handling - 👁️ Grid visibility management - **🌐 Multilingual Support** - - 🇬🇧 English (default), 🇰🇷 Korean, 🇨🇳 Chinese, 🇯🇵 Japanese - 🍪 Cookie-based language settings without URL locale prefixes - 🏷️ Multilingual display of place types @@ -179,29 +176,24 @@ docs screenshots. The `Visual QA` GitHub workflow uploads the screenshots from ### 🔐 How to obtain API keys: - **NEXT_PUBLIC_GOOGLE_MAPS_API_KEY** (Required): - - 📝 Create a project in the [Google Cloud Console](https://console.cloud.google.com/) - ✅ Enable the Maps JavaScript API, Places API, and Geocoding API - 🔑 Create an API key with appropriate restrictions - 📚 More info: [Google Maps Platform Documentation](https://developers.google.com/maps/documentation/javascript/get-api-key) - **NEXT_PUBLIC_GOOGLE_MAPS_ROADMAP_ID** (Optional): - - 🎨 Create a custom map style in the [Google Cloud Console Map Management](https://console.cloud.google.com/google/maps-apis/studio/maps) - 🆔 Use the generated Map ID for this variable - **NEXT_PUBLIC_CESIUM_ION_TOKEN** (Optional): - - 🌕 Enables Cesium ion Moon/Mars 3D Tiles in experimental planetary 3D mode - 🔑 Create a token in [Cesium ion](https://ion.cesium.com/tokens) - **NEXT_PUBLIC_CESIUM_MOON_ASSET_ID / NEXT_PUBLIC_CESIUM_MARS_ASSET_ID** (Optional): - - 🪐 Add Cesium Moon or Cesium Mars from the Cesium ion Asset Depot to your assets - 🆔 Use the corresponding asset IDs with the Cesium ion token - **GOOGLE_MAPS_NODEJS_API_KEY** (Optional): - - 🖥️ Similar to the NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, but with server-side restrictions - 🔧 Used for server-side Google Maps API calls diff --git a/apps/web/components/disable-zoom.tsx b/apps/web/components/disable-zoom.tsx index 2c33dc57..b3a435b7 100644 --- a/apps/web/components/disable-zoom.tsx +++ b/apps/web/components/disable-zoom.tsx @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import { useDisableZoom } from '@/hooks/use-disable-zoom'; +import { useDisableZoom } from "@/hooks/use-disable-zoom"; /** * A component that prevents browser zoom functionality @@ -9,7 +9,7 @@ import { useDisableZoom } from '@/hooks/use-disable-zoom'; export function DisableZoom() { // Apply the zoom disabling hook useDisableZoom(); - + // This component doesn't render anything return null; } diff --git a/apps/web/components/google-map/context/index.ts b/apps/web/components/google-map/context/index.ts index aee2834f..ecc6e725 100644 --- a/apps/web/components/google-map/context/index.ts +++ b/apps/web/components/google-map/context/index.ts @@ -1,3 +1,3 @@ -export * from './types'; -export * from './map-provider'; -export * from './use-map-context'; +export * from "./types"; +export * from "./map-provider"; +export * from "./use-map-context"; diff --git a/apps/web/components/google-map/context/use-map-context.ts b/apps/web/components/google-map/context/use-map-context.ts index 483c521f..744146f1 100644 --- a/apps/web/components/google-map/context/use-map-context.ts +++ b/apps/web/components/google-map/context/use-map-context.ts @@ -99,7 +99,7 @@ export const useMapContextState = (): MapContextType => { removeMapEventHandlers(mapInstance); setMap(null); }, - [clearAllGridLines, removeMapEventHandlers] + [clearAllGridLines, removeMapEventHandlers], ); return { diff --git a/apps/web/components/google-map/earth-3d-map.tsx b/apps/web/components/google-map/earth-3d-map.tsx index 6e95e14c..8311b40a 100644 --- a/apps/web/components/google-map/earth-3d-map.tsx +++ b/apps/web/components/google-map/earth-3d-map.tsx @@ -487,9 +487,8 @@ const Earth3DMap = ({ const markerLabel = getMarkerLabel(isEncoding, encodedCoordinates); markerRef.current.setAttribute("title", markerLabel); - const content = markerPopoverRef.current.firstElementChild as - | HTMLElement - | null; + const content = markerPopoverRef.current + .firstElementChild as HTMLElement | null; const label = content?.firstElementChild; const precision = content?.lastElementChild; if (label) { diff --git a/apps/web/components/google-map/hooks/use-air-quality.ts b/apps/web/components/google-map/hooks/use-air-quality.ts index 5cb7d78d..2654b6a9 100644 --- a/apps/web/components/google-map/hooks/use-air-quality.ts +++ b/apps/web/components/google-map/hooks/use-air-quality.ts @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from "react"; import { Coordinates } from "../types"; -import type { - WeatherData, +import type { + WeatherData, AirQualityData, - CombinedWeatherData + CombinedWeatherData, } from "@/app/api/weather-data/route"; import { useI18n } from "@/lib/i18n/i18n-context"; @@ -33,7 +33,7 @@ interface UseAirQualityOptions { */ export const useAirQuality = ( coordinates: Coordinates | null, - options: UseAirQualityOptions = {} + options: UseAirQualityOptions = {}, ) => { const { debounceMs = 1000 } = options; const { locale } = useI18n(); @@ -107,7 +107,9 @@ export const useAirQuality = ( // Handle error if both failed if (!data.airQuality && !data.weather) { - setError(data.error || "Failed to fetch weather and air quality data"); + setError( + data.error || "Failed to fetch weather and air quality data", + ); } } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch data"); diff --git a/apps/web/components/google-map/hooks/use-device-orientation.ts b/apps/web/components/google-map/hooks/use-device-orientation.ts index 72effd6e..55b13988 100644 --- a/apps/web/components/google-map/hooks/use-device-orientation.ts +++ b/apps/web/components/google-map/hooks/use-device-orientation.ts @@ -123,7 +123,8 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { }); debounceTimerRef.current = null; }, - MIN_UPDATE_INTERVAL - (currentTime - lastHeadingUpdateTimeRef.current) + MIN_UPDATE_INTERVAL - + (currentTime - lastHeadingUpdateTimeRef.current), ); } else { requestAnimationFrame(() => { @@ -131,7 +132,7 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { }); } }, - [updateHeading] + [updateHeading], ); // Request permission for device orientation @@ -142,7 +143,7 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { window.addEventListener( "deviceorientationabsolute", handleOrientation, - true + true, ); if (!window.DeviceOrientationEvent) { window.addEventListener("deviceorientation", handleOrientation, true); @@ -171,7 +172,7 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { window.addEventListener( "deviceorientation", handleOrientation, - true + true, ); orientationListenerAddedRef.current = true; } @@ -218,12 +219,12 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { window.removeEventListener( "deviceorientationabsolute", handleOrientation, - true + true, ); window.removeEventListener( "deviceorientation", handleOrientation, - true + true, ); orientationListenerAddedRef.current = false; } @@ -254,7 +255,7 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { window.addEventListener( "deviceorientationabsolute", handleOrientation, - true + true, ); if (!window.DeviceOrientationEvent) { window.addEventListener("deviceorientation", handleOrientation, true); @@ -270,12 +271,12 @@ export const useDeviceOrientation = (): UseDeviceOrientationReturn => { window.removeEventListener( "deviceorientationabsolute", handleOrientation, - true + true, ); window.removeEventListener( "deviceorientation", handleOrientation, - true + true, ); orientationListenerAddedRef.current = false; } diff --git a/apps/web/components/google-map/hooks/use-location-tracking.ts b/apps/web/components/google-map/hooks/use-location-tracking.ts index 8df4fe7b..ac2be2ba 100644 --- a/apps/web/components/google-map/hooks/use-location-tracking.ts +++ b/apps/web/components/google-map/hooks/use-location-tracking.ts @@ -17,7 +17,7 @@ export const useLocationTracking = ({ }: UseLocationTrackingProps) => { // Default value is OFF const [locationMode, setLocationMode] = useState( - LocationMode.OFF + LocationMode.OFF, ); // Location information loaded flag @@ -29,7 +29,7 @@ export const useLocationTracking = ({ // Loading state getter function const getIsLoadingLocation = useCallback( () => isLoadingLocationRef.current, - [] + [], ); // Watch position ID reference @@ -138,7 +138,7 @@ export const useLocationTracking = ({ enableHighAccuracy: true, timeout: 5000, maximumAge: 0, - } + }, ); } else if (currentLocationMode === LocationMode.TRACKING) { // TRACKING mode: start location tracking @@ -187,7 +187,7 @@ export const useLocationTracking = ({ enableHighAccuracy: true, maximumAge: 0, timeout: 5000, - } + }, ); // watchPosition ID storage diff --git a/apps/web/components/google-map/hooks/use-map-coordinates.ts b/apps/web/components/google-map/hooks/use-map-coordinates.ts index 6908dcaf..bb3c94f7 100644 --- a/apps/web/components/google-map/hooks/use-map-coordinates.ts +++ b/apps/web/components/google-map/hooks/use-map-coordinates.ts @@ -11,7 +11,7 @@ interface Coordinates { export const useMapCoordinates = ( selectedArea: Coordinates | null, - body: CelestialBody = "earth" + body: CelestialBody = "earth", ) => { const { locale } = useI18n(); const [encodedCoordinates, setEncodedCoordinates] = useState(""); diff --git a/apps/web/components/google-map/map-search.tsx b/apps/web/components/google-map/map-search.tsx index 45569558..245286d5 100644 --- a/apps/web/components/google-map/map-search.tsx +++ b/apps/web/components/google-map/map-search.tsx @@ -74,7 +74,7 @@ const MapSearch: React.FC = ({ searchInputRef.current, { fields: ["address_components", "geometry", "name", "formatted_address"], - } + }, ); autocompleteRef.current.bindTo("bounds", map); @@ -87,14 +87,14 @@ const MapSearch: React.FC = ({ if (!place?.geometry?.location) { window.alert( - `No details available for: ${place?.name || "this place"}` + `No details available for: ${place?.name || "this place"}`, ); return; } setQuery(place.name ?? searchInputRef.current?.value ?? ""); onPlaceSelect(place); - } + }, ); return () => { @@ -162,7 +162,7 @@ const MapSearch: React.FC = ({ } setPlacePredictions(nextPredictions); - } + }, ); }, 300); @@ -204,8 +204,9 @@ const MapSearch: React.FC = ({ const nextSuggestions = results.slice(0, 5); groundSuggestionCacheRef.current.set(cacheKey, nextSuggestions); if (groundSuggestionCacheRef.current.size > 30) { - const oldestQuery = groundSuggestionCacheRef.current.keys().next() - .value; + const oldestQuery = groundSuggestionCacheRef.current + .keys() + .next().value; if (oldestQuery) { groundSuggestionCacheRef.current.delete(oldestQuery); } @@ -243,14 +244,15 @@ const MapSearch: React.FC = ({ }; const selectPlacePrediction = async ( - prediction: google.maps.places.AutocompletePrediction + prediction: google.maps.places.AutocompletePrediction, ) => { if (isGroundSearchLoading) return; predictionRequestIdRef.current += 1; groundSuggestionRequestIdRef.current += 1; - suppressedPredictionQueryRef.current = - prediction.description.trim().toLocaleLowerCase(); + suppressedPredictionQueryRef.current = prediction.description + .trim() + .toLocaleLowerCase(); setQuery(prediction.description); setPlacePredictions([]); setGroundSuggestions([]); @@ -411,7 +413,8 @@ const MapSearch: React.FC = ({ {result.label} - {result.code ?? `${result.lat.toFixed(4)}, ${result.lng.toFixed(4)}`} + {result.code ?? + `${result.lat.toFixed(4)}, ${result.lng.toFixed(4)}`} diff --git a/apps/web/components/google-map/place-details/helpers.ts b/apps/web/components/google-map/place-details/helpers.ts index 30646c59..216145c1 100644 --- a/apps/web/components/google-map/place-details/helpers.ts +++ b/apps/web/components/google-map/place-details/helpers.ts @@ -9,12 +9,38 @@ export const getCurrentDayIndex = (): number => new Date().getDay(); * Google Places API (e.g. "Monday: 9:00 AM – 5:00 PM", "월요일: …"). * Returns -1 when the day cannot be determined. */ -export const getDayIndexFromString = (dayString: string | undefined): number => { +export const getDayIndexFromString = ( + dayString: string | undefined, +): number => { if (!dayString) return -1; - const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - const koDays = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"]; - const cnDays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]; + const days = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + const koDays = [ + "일요일", + "월요일", + "화요일", + "수요일", + "목요일", + "금요일", + "토요일", + ]; + const cnDays = [ + "星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + ]; for (let i = 0; i < days.length; i++) { if (dayString.startsWith(days[i]!)) return i; @@ -34,7 +60,7 @@ export const getDayIndexFromString = (dayString: string | undefined): number => * Works with both `periods` and `weekday_text` data from the Places API. */ export const isOpenNow = ( - placeDetails: google.maps.places.PlaceResult | null + placeDetails: google.maps.places.PlaceResult | null, ): boolean => { try { if (placeDetails?.opening_hours?.periods) { @@ -53,7 +79,7 @@ export const isOpenNow = ( } const todayPeriods = placeDetails.opening_hours.periods.filter( - (period) => period.open && period.open.day === day + (period) => period.open && period.open.day === day, ); for (const period of todayPeriods) { @@ -114,17 +140,37 @@ export const isOpenNow = ( let closeHour24: number, closeMinute: string | undefined; if (pattern.source.includes("AM|PM")) { - const [, openHour, _openMinute, openAmPm, closeHour, _closeMinute, closeAmPm] = match; + const [ + , + openHour, + _openMinute, + openAmPm, + closeHour, + _closeMinute, + closeAmPm, + ] = match; openHour24 = parseInt(openHour!); - if (openAmPm!.toUpperCase() === "PM" && openHour24 < 12) openHour24 += 12; - if (openAmPm!.toUpperCase() === "AM" && openHour24 === 12) openHour24 = 0; + if (openAmPm!.toUpperCase() === "PM" && openHour24 < 12) + openHour24 += 12; + if (openAmPm!.toUpperCase() === "AM" && openHour24 === 12) + openHour24 = 0; closeHour24 = parseInt(closeHour!); - if (closeAmPm!.toUpperCase() === "PM" && closeHour24 < 12) closeHour24 += 12; - if (closeAmPm!.toUpperCase() === "AM" && closeHour24 === 12) closeHour24 = 0; + if (closeAmPm!.toUpperCase() === "PM" && closeHour24 < 12) + closeHour24 += 12; + if (closeAmPm!.toUpperCase() === "AM" && closeHour24 === 12) + closeHour24 = 0; openMinute = _openMinute; closeMinute = _closeMinute; } else if (pattern.source.includes("오전|오후")) { - const [, openAmPm, openHour, _openMinute, closeAmPm, closeHour, _closeMinute] = match; + const [ + , + openAmPm, + openHour, + _openMinute, + closeAmPm, + closeHour, + _closeMinute, + ] = match; openHour24 = parseInt(openHour!); if (openAmPm === "오후" && openHour24 < 12) openHour24 += 12; if (openAmPm === "오전" && openHour24 === 12) openHour24 = 0; @@ -142,8 +188,10 @@ export const isOpenNow = ( } const currentTime = currentHour * 60 + currentMinute; - const openTime = openHour24 * 60 + (openMinute ? parseInt(openMinute) : 0); - const closeTime = closeHour24 * 60 + (closeMinute ? parseInt(closeMinute) : 0); + const openTime = + openHour24 * 60 + (openMinute ? parseInt(openMinute) : 0); + const closeTime = + closeHour24 * 60 + (closeMinute ? parseInt(closeMinute) : 0); if (openTime < closeTime) { if (currentTime >= openTime && currentTime < closeTime) return true; @@ -164,7 +212,10 @@ export const isOpenNow = ( }; /** Returns the translated display name for a Google Places type key. */ -export const getPlaceTypeName = (type: string | undefined, locale: Locale): string => { +export const getPlaceTypeName = ( + type: string | undefined, + locale: Locale, +): string => { if (!type) return ""; const translatedType = (placeTypes[locale] as PlaceTypesRecord)[type]; return translatedType || type.replace(/_/g, " "); diff --git a/apps/web/components/google-map/place-details/index.tsx b/apps/web/components/google-map/place-details/index.tsx index 63457cd8..20884011 100644 --- a/apps/web/components/google-map/place-details/index.tsx +++ b/apps/web/components/google-map/place-details/index.tsx @@ -17,7 +17,12 @@ import { } from "react-icons/fa"; import { PlaceDetailsProps } from "./types"; -import { getCurrentDayIndex, getDayIndexFromString, isOpenNow, getPlaceTypeName } from "./helpers"; +import { + getCurrentDayIndex, + getDayIndexFromString, + isOpenNow, + getPlaceTypeName, +} from "./helpers"; import { usePlaceDetails } from "./use-place-details"; import { useGroundCode } from "./use-ground-code"; import { useCopy } from "./use-copy"; @@ -46,15 +51,19 @@ const PlaceDetails: React.FC = ({ map, placeId, visible, - resetPhotoState + resetPhotoState, ); const { groundCode, isLoadingGroundCode } = useGroundCode(location, visible); - const { address: addressCopy, groundCode: groundCodeCopy, phone: phoneCopy } = useCopy( + const { + address: addressCopy, + groundCode: groundCodeCopy, + phone: phoneCopy, + } = useCopy( useCallback(() => placeDetails?.formatted_address, [placeDetails]), useCallback(() => groundCode || undefined, [groundCode]), - useCallback(() => placeDetails?.formatted_phone_number, [placeDetails]) + useCallback(() => placeDetails?.formatted_phone_number, [placeDetails]), ); const handleImageError = (index: number) => { @@ -108,7 +117,10 @@ const PlaceDetails: React.FC = ({
- {getPlaceTypeName(placeDetails.types?.[0], locale as Locale)} + {getPlaceTypeName( + placeDetails.types?.[0], + locale as Locale, + )}
)} @@ -157,7 +169,10 @@ const PlaceDetails: React.FC = ({
)}