Add developer API, Prometheus metrics, and community links
This commit is contained in:
8
app/api/dev/keys/route.ts
Normal file
8
app/api/dev/keys/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { developerKeys } from "@/lib/developer-key-management";
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
export const GET = developerKeys;
|
||||
export const POST = developerKeys;
|
||||
export const DELETE = developerKeys;
|
||||
export const PUT = developerKeys;
|
||||
export const PATCH = developerKeys;
|
||||
10
app/api/dev/openapi.json/route.ts
Normal file
10
app/api/dev/openapi.json/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { developerOpenApi } from "@/shared/developer-openapi";
|
||||
export const dynamic = "force-static";
|
||||
export function GET() {
|
||||
return Response.json(developerOpenApi, {
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
});
|
||||
}
|
||||
3
app/api/dev/route.ts
Normal file
3
app/api/dev/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function GET(request: Request) {
|
||||
return Response.redirect(new URL("/dev/api", request.url), 307);
|
||||
}
|
||||
17
app/api/dev/v1/[...path]/route.ts
Normal file
17
app/api/dev/v1/[...path]/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { developerApi } from "@/lib/developer-api";
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
export const maxDuration = 30;
|
||||
async function handle(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ path: string[] }> },
|
||||
) {
|
||||
return developerApi(request, (await params).path);
|
||||
}
|
||||
export {
|
||||
handle as GET,
|
||||
handle as POST,
|
||||
handle as PATCH,
|
||||
handle as PUT,
|
||||
handle as DELETE,
|
||||
};
|
||||
16
app/api/metrics/route.ts
Normal file
16
app/api/metrics/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { admin, required } from "@/lib/supabase";
|
||||
import { metricsResponse } from "@/lib/prometheus-response";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
export const maxDuration = 30;
|
||||
export function GET(request: Request) {
|
||||
return metricsResponse(request, async (tokenHash) => {
|
||||
const result = await admin().rpc("read_prometheus_metrics", {
|
||||
p_token_hash: tokenHash,
|
||||
});
|
||||
// NULL is an invalid or disabled credential, not a missing database record.
|
||||
if (result.error) required(result);
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
@@ -384,7 +384,7 @@ html[data-theme="light"] .site-cursor-dot span {
|
||||
background: #273b21;
|
||||
box-shadow: 0 0 1px #fff;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 1200px) {
|
||||
.site-header {
|
||||
height: auto;
|
||||
}
|
||||
@@ -436,7 +436,7 @@ html[data-theme="light"] .site-cursor-dot span {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 380px) {
|
||||
@media (max-width: 600px) {
|
||||
.header-actions {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
|
||||
225
app/dev/api/page.module.css
Normal file
225
app/dev/api/page.module.css
Normal file
@@ -0,0 +1,225 @@
|
||||
.intro {
|
||||
max-width: 780px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 24px 0 36px;
|
||||
}
|
||||
.stack {
|
||||
display: grid;
|
||||
gap: 32px;
|
||||
}
|
||||
.panel {
|
||||
padding: clamp(20px, 4vw, 32px);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: var(--panel);
|
||||
min-width: 0;
|
||||
}
|
||||
.panel h2 {
|
||||
font-size: clamp(24px, 3vw, 32px);
|
||||
}
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.heading h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(140px, 200px);
|
||||
gap: 20px;
|
||||
}
|
||||
.fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.fieldset legend {
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.scopes {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.scope {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.scope input {
|
||||
margin-top: 5px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
accent-color: var(--green);
|
||||
}
|
||||
.scope strong,
|
||||
.scope small {
|
||||
display: block;
|
||||
}
|
||||
.scope small {
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
.token {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--green);
|
||||
border-radius: 12px;
|
||||
background: var(--panel2);
|
||||
}
|
||||
.token input {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.keyList {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
margin: 24px 0 0;
|
||||
}
|
||||
.key {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
.key h3 {
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
.key dl {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 6px 18px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.key dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
.key dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.key code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.notice {
|
||||
padding: 14px 18px;
|
||||
border-left: 3px solid var(--green);
|
||||
background: var(--panel2);
|
||||
margin: 20px 0;
|
||||
}
|
||||
.error {
|
||||
color: var(--text);
|
||||
border-left-color: #ed7878;
|
||||
}
|
||||
.docs {
|
||||
scroll-margin-top: 120px;
|
||||
}
|
||||
.docs h3 {
|
||||
margin-top: 32px;
|
||||
}
|
||||
.docs p,
|
||||
.docs li {
|
||||
line-height: 1.8;
|
||||
}
|
||||
.docs ul,
|
||||
.docs ol {
|
||||
padding-left: 24px;
|
||||
}
|
||||
.docs ul {
|
||||
list-style: disc;
|
||||
}
|
||||
.docs ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
.docs a {
|
||||
color: var(--green);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.docs code {
|
||||
overflow-wrap: anywhere;
|
||||
font-family: monospace;
|
||||
}
|
||||
.code {
|
||||
overflow-x: auto;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--bg);
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
max-width: 100%;
|
||||
}
|
||||
.code code {
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.tableWrap {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: top;
|
||||
}
|
||||
.table th {
|
||||
color: var(--muted);
|
||||
}
|
||||
.table td:first-child {
|
||||
min-width: 150px;
|
||||
}
|
||||
.table td:last-child {
|
||||
min-width: 200px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.key dl {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
.key dd {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.code {
|
||||
padding: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
23
app/dev/api/page.tsx
Normal file
23
app/dev/api/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { DeveloperPanel } from "@/components/DeveloperPanel";
|
||||
import { user } from "@/lib/supabase";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Developer API",
|
||||
description: "Create and manage scoped MineGeo API keys.",
|
||||
};
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function DeveloperApiPage() {
|
||||
const account = await user();
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<DeveloperPanel signedIn={Boolean(account)} />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import Link from "next/link";
|
||||
import { DeveloperDocumentation } from "@/components/DeveloperDocumentation";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import guides from "@/shared/player-docs.json";
|
||||
import styles from "./page.module.css";
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
|
||||
export const metadata = { title: "Player docs" };
|
||||
|
||||
@@ -76,9 +78,18 @@ export default async function Docs({
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const { page = "welcome" } = await searchParams;
|
||||
const guide = guides.find((item) => item.slug === page);
|
||||
const { locale } = await getI18n();
|
||||
const pages = [
|
||||
...guides,
|
||||
{
|
||||
slug: "developer-api",
|
||||
title: locale === "ru" ? "API для разработчиков" : "Developer API",
|
||||
markdown: "",
|
||||
},
|
||||
];
|
||||
const guide = pages.find((item) => item.slug === page);
|
||||
if (!guide) notFound();
|
||||
const index = guides.indexOf(guide);
|
||||
const index = pages.indexOf(guide);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
@@ -86,7 +97,7 @@ export default async function Docs({
|
||||
<aside>
|
||||
<h2>Player docs</h2>
|
||||
<nav aria-label="Documentation pages" className={styles.menu}>
|
||||
{guides.map((item) => (
|
||||
{pages.map((item) => (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={`/docs?page=${item.slug}`}
|
||||
@@ -98,16 +109,20 @@ export default async function Docs({
|
||||
</nav>
|
||||
</aside>
|
||||
<article className={`prose ${styles.article}`}>
|
||||
{content(guide.markdown)}
|
||||
{page === "developer-api" ? (
|
||||
<DeveloperDocumentation locale={locale} />
|
||||
) : (
|
||||
content(guide.markdown)
|
||||
)}
|
||||
<div className={styles.paging}>
|
||||
{index > 0 && (
|
||||
<Link href={`/docs?page=${guides[index - 1].slug}`}>
|
||||
← {guides[index - 1].title}
|
||||
<Link href={`/docs?page=${pages[index - 1].slug}`}>
|
||||
← {pages[index - 1].title}
|
||||
</Link>
|
||||
)}
|
||||
{index < guides.length - 1 && (
|
||||
<Link href={`/docs?page=${guides[index + 1].slug}`}>
|
||||
{guides[index + 1].title} →
|
||||
{index < pages.length - 1 && (
|
||||
<Link href={`/docs?page=${pages[index + 1].slug}`}>
|
||||
{pages[index + 1].title} →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
@import "tailwindcss" source(none);
|
||||
@source ".";
|
||||
@source "../components";
|
||||
.metrics-steps {
|
||||
list-style: decimal;
|
||||
padding-left: 24px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.metrics-steps li { margin: 12px 0; }
|
||||
.metrics-config { font-family: monospace; font-size: 12px; overflow-wrap: normal; white-space: pre; }
|
||||
:root {
|
||||
--bg: #0b100f;
|
||||
--panel: #121b17;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async function Review() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content">
|
||||
<main className="section page-content moderation-page">
|
||||
<span className="eyebrow">{t("THE COMMUNITY’S COMPASS")}</span>
|
||||
<h1>{t("Map moderation.")}</h1>
|
||||
<p className="lead">
|
||||
|
||||
@@ -125,6 +125,29 @@ html[lang="ru"] .mode-row h3 span {
|
||||
.auth-tabs button {
|
||||
border-radius: var(--radius-control);
|
||||
}
|
||||
/* Keep catalog cards readable instead of squeezing long map names. */
|
||||
.cards-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
|
||||
}
|
||||
.map-card .card-body .map-credit {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
min-height: 0;
|
||||
-webkit-line-clamp: unset;
|
||||
}
|
||||
.map-card .card-body .eyebrow {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.search-field:focus-within {
|
||||
outline: 2px solid var(--green);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.dashboard-toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dashboard-toolbar > * {
|
||||
min-width: 0;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
.site-header {
|
||||
height: auto;
|
||||
@@ -209,12 +232,38 @@ html[lang="ru"] .mode-row h3 span {
|
||||
.home-panel + .home-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
.map-card .card-body p {
|
||||
display: -webkit-box;
|
||||
}
|
||||
.map-card .card-meta span:last-child {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.moderator-settings {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.moderation-page :is(.tool-card, .draft-row) {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.moderator-setting-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.moderator-setting-row h3 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.moderator-setting-row p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.moderator-setting-row .button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.moderator-settings { margin-bottom: 28px; }
|
||||
.moderator-setting-row { display: flex; gap: 24px; justify-content: space-between; align-items: center; margin-top: 24px; }
|
||||
.moderator-setting-row h3 { margin: 0 0 8px; }
|
||||
.moderator-setting-row p { margin: 6px 0; }
|
||||
.moderator-setting-row .button { flex-shrink: 0; }
|
||||
@media (max-width: 600px) {
|
||||
.moderator-setting-row { align-items: stretch; flex-direction: column; }
|
||||
.moderator-setting-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user