Merge pull request 'feat: Report Bug button that creates Gitea issues' (#15) from feat/report-bug into main
Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
commit
3c0fee3ec8
6 files changed
+331
-2
No files matched your search
@@ -185,4 +185,12 @@ export const api = {
|
|||||||
resetFans: (gpuIndex: number) => post("/fans/reset", undefined, gpuIndex),
|
resetFans: (gpuIndex: number) => post("/fans/reset", undefined, gpuIndex),
|
||||||
setFanSpeed: (fanPct: number, gpuIndex: number, fan?: number | null) =>
|
setFanSpeed: (fanPct: number, gpuIndex: number, fan?: number | null) =>
|
||||||
post("/fans/speed", { fan_pct: fanPct, fan: fan ?? null }, gpuIndex),
|
post("/fans/speed", { fan_pct: fanPct, fan: fan ?? null }, gpuIndex),
|
||||||
|
|
||||||
|
/** Report Bug (Gitea) */
|
||||||
|
reportBugStatus: () => get<{ configured: boolean }>("/report-bug/status"),
|
||||||
|
reportBug: (title: string, body: string) =>
|
||||||
|
post<{ ok: boolean; issue_number: number | null; issue_url: string | null }>(
|
||||||
|
"/report-bug",
|
||||||
|
{ title, body },
|
||||||
|
),
|
||||||
};
|
};
|
||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogOut,
|
LogOut,
|
||||||
User,
|
User,
|
||||||
|
Bug,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { GpuInfo, MonitoringSample } from "../../types.js";
|
import type { GpuInfo, MonitoringSample } from "../../types.js";
|
||||||
import { fmt } from "../../utils/units.js";
|
import { fmt } from "../../utils/units.js";
|
||||||
import { useCurveStore } from "../../store/curveStore.js";
|
import { useCurveStore } from "../../store/curveStore.js";
|
||||||
|
import { api } from "../../api/client.js";
|
||||||
|
import { ReportBugDialog } from "../common/ReportBugDialog.js";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -43,8 +46,17 @@ export function StatusBar({
|
|||||||
const { availableGpus, selectedGpuIndex, setSelectedGpuIndex } =
|
const { availableGpus, selectedGpuIndex, setSelectedGpuIndex } =
|
||||||
useCurveStore();
|
useCurveStore();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [bugReportConfigured, setBugReportConfigured] = useState(false);
|
||||||
|
const [reportBugOpen, setReportBugOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.reportBugStatus()
|
||||||
|
.then((s) => setBugReportConfigured(s.configured))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
if (
|
if (
|
||||||
@@ -169,8 +181,18 @@ export function StatusBar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: user + connection status */}
|
{/* Right: report bug + user + connection status */}
|
||||||
<div className="flex items-center gap-3 shrink-0">
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
|
{bugReportConfigured && (
|
||||||
|
<button
|
||||||
|
onClick={() => setReportBugOpen(true)}
|
||||||
|
title="Report a bug (creates a Gitea issue)"
|
||||||
|
className="flex items-center gap-1.5 px-2 py-1 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors"
|
||||||
|
>
|
||||||
|
<Bug size={14} />
|
||||||
|
<span className="hidden sm:inline text-xs">Report Bug</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{user && (
|
{user && (
|
||||||
<div className="flex items-center gap-1.5 text-zinc-400">
|
<div className="flex items-center gap-1.5 text-zinc-400">
|
||||||
<User size={14} className="text-zinc-500" />
|
<User size={14} className="text-zinc-500" />
|
||||||
@@ -192,6 +214,9 @@ export function StatusBar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{reportBugOpen && (
|
||||||
|
<ReportBugDialog onClose={() => setReportBugOpen(false)} />
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Bug, Loader } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { api } from "../../api/client.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportBugDialog({ onClose }: Props) {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [body, setBody] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [done, setDone] = useState<{
|
||||||
|
number: number | null;
|
||||||
|
url: string | null;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!title.trim() || submitting) return;
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const res = await api.reportBug(title.trim(), body.trim());
|
||||||
|
setDone({ number: res.issue_number, url: res.issue_url });
|
||||||
|
toast.success(
|
||||||
|
res.issue_number != null ? `Issue #${res.issue_number} created` : "Issue created",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : "Failed to create issue");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-6 w-[480px] max-w-[90vw]">
|
||||||
|
{done ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-zinc-100 font-semibold text-sm mb-2 flex items-center gap-2">
|
||||||
|
<Bug size={16} className="text-emerald-400" /> Issue created
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 text-xs mb-4">
|
||||||
|
{done.number != null ? `Issue #${done.number} ` : "The issue "}
|
||||||
|
was filed on Gitea with the auto-collected system context.{" "}
|
||||||
|
{done.url ? (
|
||||||
|
<a
|
||||||
|
href={done.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-pink-400 hover:underline"
|
||||||
|
>
|
||||||
|
Open it →
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
"."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-3 py-1.5 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-xs font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="text-zinc-100 font-semibold text-sm mb-1 flex items-center gap-2">
|
||||||
|
<Bug size={16} className="text-pink-400" /> Report a bug
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 text-xs mb-4">
|
||||||
|
Creates a Gitea issue. System info (version, GPU, active profile,
|
||||||
|
recent log) is attached automatically.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Title / Subject (required)"
|
||||||
|
maxLength={200}
|
||||||
|
className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:border-pink-500 mb-3"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
value={body}
|
||||||
|
onChange={(e) => setBody(e.target.value)}
|
||||||
|
placeholder="What happened? Steps to reproduce, expected vs. actual…"
|
||||||
|
rows={6}
|
||||||
|
maxLength={5000}
|
||||||
|
className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:border-pink-500 mb-4 resize-y"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-3 py-1.5 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs transition-colors disabled:opacity-40"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!title.trim() || submitting}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-pink-600 hover:bg-pink-500 text-white text-xs font-semibold transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{submitting && <Loader size={12} className="animate-spin" />}
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2309,6 +2309,9 @@ def main():
|
|||||||
"ssl_certfile",
|
"ssl_certfile",
|
||||||
"ssl_keyfile",
|
"ssl_keyfile",
|
||||||
"allow_api_shutdown",
|
"allow_api_shutdown",
|
||||||
|
"gitea_url",
|
||||||
|
"gitea_repo",
|
||||||
|
"gitea_token",
|
||||||
):
|
):
|
||||||
if key in data:
|
if key in data:
|
||||||
setattr(cfg, key, data[key])
|
setattr(cfg, key, data[key])
|
||||||
|
|||||||
@@ -59,6 +59,20 @@ class Config:
|
|||||||
# Key = stable GPU identifier (same as auto_load_profiles).
|
# Key = stable GPU identifier (same as auto_load_profiles).
|
||||||
power_cap_modes: dict[str, str] = field(default_factory=dict)
|
power_cap_modes: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# Gitea "Report Bug" integration: the built-in defaults point at the
|
||||||
|
# upstream project's Gitea, so the feature works out of the box for every
|
||||||
|
# install — no configuration required. Forks can override these in
|
||||||
|
# config.json (own instance/repo/token); setting gitea_token to ""
|
||||||
|
# disables the feature. The token is scoped to write:issue on a public
|
||||||
|
# repo, so it can only create/read issues there.
|
||||||
|
gitea_url: str = "https://gitea.zephyre.one"
|
||||||
|
gitea_repo: str = "Pakobbix/nvcurve"
|
||||||
|
# Intentionally embedded: zero-config bug reporting for a public repo.
|
||||||
|
# The token can only create/read issues on Pakobbix/nvcurve (scope
|
||||||
|
# write:issue) and is revocable/rotatable by the repo owner.
|
||||||
|
# pi-lens-ignore: S105
|
||||||
|
gitea_token: str = "bfe416043b633f8f3c4df103272ad0ccf91c8f22"
|
||||||
|
|
||||||
|
|
||||||
# Module-level default config instance.
|
# Module-level default config instance.
|
||||||
default_config = Config()
|
default_config = Config()
|
||||||
|
|||||||
+160
-1
@@ -10,16 +10,20 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
from contextlib import asynccontextmanager, suppress
|
from contextlib import asynccontextmanager, suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from . import auth
|
from . import auth
|
||||||
from .config import Config, default_config, tls_enabled
|
from .config import Config, default_config, tls_enabled
|
||||||
from .hal.dashboard import get_dashboard_info
|
from .hal.dashboard import get_dashboard_info
|
||||||
@@ -848,6 +852,11 @@ class ProcessKillRequest(BaseModel):
|
|||||||
parent: bool = False
|
parent: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ReportBugRequest(BaseModel):
|
||||||
|
title: str
|
||||||
|
body: str = ""
|
||||||
|
|
||||||
|
|
||||||
# ── Helper: run blocking HAL call in thread pool ──────────────────────────────
|
# ── Helper: run blocking HAL call in thread pool ──────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -2141,6 +2150,157 @@ async def api_shutdown():
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Report Bug (Gitea) ────────────────────────────────────────────────────────
|
||||||
|
# The Gitea token is held server-side only and never reaches the browser.
|
||||||
|
# Recommended setup: a dedicated bot user with write access to just the target
|
||||||
|
# repo and a token scoped to write:issue, so a leaked token cannot read code
|
||||||
|
# or push.
|
||||||
|
_BUG_REPORT_COOLDOWN_S = 300.0 # per-client rate limit
|
||||||
|
_bug_report_last: dict[str, float] = {}
|
||||||
|
_SERVER_LOG_FILE = "/var/log/nvcurve-server.log"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/report-bug/status", tags=["Report Bug"], responses={**R_AUTH})
|
||||||
|
async def api_report_bug_status():
|
||||||
|
"""Whether the Report Bug feature is configured (token set server-side)."""
|
||||||
|
cfg: Config = _state["config"]
|
||||||
|
return {
|
||||||
|
"configured": bool(cfg.gitea_url and cfg.gitea_repo and cfg.gitea_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_bug_report_body(body: str) -> str:
|
||||||
|
"""Compose the Gitea issue body: user report + auto-collected context."""
|
||||||
|
lines: list[str] = []
|
||||||
|
if body:
|
||||||
|
lines.append(body)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("_Reported via the NVCurve web UI.")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## System context (auto-collected)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"- nvcurve version: {__version__}")
|
||||||
|
lines.append(f"- hostname: {socket.gethostname()}")
|
||||||
|
for gpu_index, g_state in sorted(_state["gpus"].items()):
|
||||||
|
name = g_state.get("gpu_name") or f"GPU {gpu_index}"
|
||||||
|
lines.append(f"- GPU {gpu_index}: {name}")
|
||||||
|
if g_state.get("last_offsets"):
|
||||||
|
offsets = g_state["last_offsets"]
|
||||||
|
non_zero = [(i, v) for i, v in enumerate(offsets) if v]
|
||||||
|
if non_zero:
|
||||||
|
vals = [v for _, v in non_zero]
|
||||||
|
lines.append(
|
||||||
|
f" - last applied offsets: {len(non_zero)} non-zero points "
|
||||||
|
f"(min {min(vals)} kHz, max {max(vals)} kHz)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(" - last applied offsets: none (all zero)")
|
||||||
|
if g_state.get("active_profile"):
|
||||||
|
lines.append(f" - active profile: {g_state['active_profile']}")
|
||||||
|
# Best-effort tail of the server log (daemon mode writes to this file).
|
||||||
|
try:
|
||||||
|
with open(_SERVER_LOG_FILE, "rb") as f:
|
||||||
|
f.seek(0, os.SEEK_END)
|
||||||
|
size = f.tell()
|
||||||
|
f.seek(max(0, size - 8192))
|
||||||
|
tail = f.read().decode("utf-8", errors="replace")
|
||||||
|
log_lines = tail.splitlines()[-40:]
|
||||||
|
if log_lines:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("<details><summary>Recent server log (last 40 lines)</summary>")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("```")
|
||||||
|
lines.extend(log_lines)
|
||||||
|
lines.append("```")
|
||||||
|
lines.append("</details>")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/api/report-bug",
|
||||||
|
tags=["Report Bug"],
|
||||||
|
responses={**R_AUTH, 429: {"description": "Rate limit: report too soon."}, 502: {"description": "Gitea unreachable or token rejected."}, 503: {"description": "Not configured."}},
|
||||||
|
)
|
||||||
|
async def api_report_bug(req: ReportBugRequest, request: Request):
|
||||||
|
"""Create a Gitea issue from a bug report.
|
||||||
|
|
||||||
|
Auto-collected context (version, GPUs, active profile, recent log tail)
|
||||||
|
is appended to the issue body. The Gitea token never leaves the server.
|
||||||
|
"""
|
||||||
|
cfg: Config = _state["config"]
|
||||||
|
if not (cfg.gitea_url and cfg.gitea_repo and cfg.gitea_token):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Report Bug is not configured (gitea_url/gitea_repo/gitea_token)",
|
||||||
|
)
|
||||||
|
|
||||||
|
title = req.title.strip()[:200]
|
||||||
|
if not title:
|
||||||
|
raise HTTPException(status_code=422, detail="Title is required")
|
||||||
|
body = req.body.strip()[:5000]
|
||||||
|
|
||||||
|
# Rate limit: one report per client per cooldown window.
|
||||||
|
now = time.monotonic()
|
||||||
|
try:
|
||||||
|
key = _client_ip(request, cfg.trusted_proxies)
|
||||||
|
except Exception:
|
||||||
|
key = "unknown"
|
||||||
|
for stale in [k for k, t in _bug_report_last.items() if now - t > _BUG_REPORT_COOLDOWN_S * 2]:
|
||||||
|
del _bug_report_last[stale]
|
||||||
|
last = _bug_report_last.get(key)
|
||||||
|
if last is not None and now - last < _BUG_REPORT_COOLDOWN_S:
|
||||||
|
wait = int(_BUG_REPORT_COOLDOWN_S - (now - last))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=429, detail=f"Too soon — try again in {wait}s"
|
||||||
|
)
|
||||||
|
_bug_report_last[key] = now
|
||||||
|
|
||||||
|
url = f"{cfg.gitea_url.rstrip('/')}/api/v1/repos/{cfg.gitea_repo}/issues"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
res = await client.post(
|
||||||
|
url,
|
||||||
|
json={"title": title, "body": _build_bug_report_body(body)},
|
||||||
|
headers={"Authorization": f"token {cfg.gitea_token}"},
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
log.warning("Report Bug: Gitea request failed: %s", exc)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502, detail="Could not reach Gitea"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if res.status_code in (401, 403):
|
||||||
|
log.warning(
|
||||||
|
"Report Bug: Gitea rejected the token (HTTP %d)", res.status_code
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail="Gitea rejected the configured token (invalid or insufficient scope)",
|
||||||
|
)
|
||||||
|
if res.status_code >= 400:
|
||||||
|
log.warning(
|
||||||
|
"Report Bug: Gitea error HTTP %d: %s", res.status_code, res.text[:300]
|
||||||
|
)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502, detail=f"Gitea error (HTTP {res.status_code})"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = res.json()
|
||||||
|
log.info(
|
||||||
|
"Report Bug: created issue #%s (%s)",
|
||||||
|
data.get("number"),
|
||||||
|
data.get("html_url"),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"issue_number": data.get("number"),
|
||||||
|
"issue_url": data.get("html_url"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/snapshot/save", tags=["Snapshots"], responses={**R_AUTH, **R_GPU, 500: R_HW_FAILED})
|
@app.post("/api/snapshot/save", tags=["Snapshots"], responses={**R_AUTH, **R_GPU, 500: R_HW_FAILED})
|
||||||
async def api_snapshot_save(gpu_index: int = 0):
|
async def api_snapshot_save(gpu_index: int = 0):
|
||||||
"""Save a ClockBoostTable snapshot."""
|
"""Save a ClockBoostTable snapshot."""
|
||||||
@@ -2409,7 +2569,6 @@ def run(
|
|||||||
When both ssl_certfile and ssl_keyfile are given (either here or in the
|
When both ssl_certfile and ssl_keyfile are given (either here or in the
|
||||||
config), the server serves HTTPS and the session cookie is Secure.
|
config), the server serves HTTPS and the session cookie is Secure.
|
||||||
"""
|
"""
|
||||||
import socket
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|||||||
Reference in new issue
Block a user