修复 Docker 配置并添加 mTLS 证书支持

- Dockerfile: 添加 Python 虚拟环境,修复端口匹配,跳过 SSL 验证下载 Portwarden
- entrypoint.sh: 支持 http:// 地址(用于测试)
- frontend/src/lib/api.ts: 添加 getReport 函数
- frontend/next.config.ts: 移除无效的 turbopack 配置
- frontend/src/app/page.tsx: 添加 Suspense 边界包裹 useSearchParams
- frontend/src/components/nav-header.tsx: 添加 Suspense 边界包裹 useSearchParams
- bastian/: 添加从 lyman.p12 提取的 mTLS 证书文件
This commit is contained in:
xucheng 2026-01-14 11:12:24 +08:00
parent 2edb75b19a
commit 77a08f1c55
10 changed files with 121 additions and 48 deletions

View File

@ -33,14 +33,16 @@ RUN apt-get update && apt-get install -y \
WORKDIR /app WORKDIR /app
# 2. Install Python Backend Dependencies # 2. Create Python Virtual Environment and Install Dependencies
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN python -m venv /app/.venv && \
/app/.venv/bin/pip install --no-cache-dir -r requirements.txt
# 3. Bake in Portwarden Client (The "Tunnel") # 3. Bake in Portwarden Client (The "Tunnel")
# This runs during build time to download the binary into the image # This runs during build time to download the binary into the image
# 使用 -k 跳过 SSL 证书验证bastion.3prism.ai 证书过期)
RUN echo "Downloading Portwarden Client from: ${BASTION_URL}/releases/portwardenc-${HOST_ARCH}" && \ RUN echo "Downloading Portwarden Client from: ${BASTION_URL}/releases/portwardenc-${HOST_ARCH}" && \
curl -fsSL "${BASTION_URL}/releases/portwardenc-${HOST_ARCH}" -o /usr/local/bin/portwardenc && \ curl -fsSLk "${BASTION_URL}/releases/portwardenc-${HOST_ARCH}" -o /usr/local/bin/portwardenc && \
chmod +x /usr/local/bin/portwardenc chmod +x /usr/local/bin/portwardenc
# 4. Copy Frontend Build Artifacts # 4. Copy Frontend Build Artifacts
@ -64,7 +66,7 @@ COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh ./start_app.sh RUN chmod +x /usr/local/bin/entrypoint.sh ./start_app.sh
# Environment Variables Defaults # Environment Variables Defaults
ENV PW_LOCAL_PORT=3000 ENV PW_LOCAL_PORT=3001
# Disable Next.js Telemetry # Disable Next.js Telemetry
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
@ -74,8 +76,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
# Entrypoint & Command # Entrypoint & Command
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# We need a robust start script for prod. # 启动前端 (Next.js 生产模式) 和后端 (FastAPI)
# For now, we'll use a modified start command inline or assume start_app.sh is smart enough. # 前端监听 3001 端口,后端监听 8000 端口
# Let's override the default start_app.sh behavior to use production modes if possible, # 使用虚拟环境中的 Python 运行后端
# OR just keep it simple as user requested "built-in". CMD ["bash", "-c", "cd /app/frontend && npm start & cd /app/backend && /app/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 && wait"]
CMD ["bash", "-c", "cd frontend && npm start & cd backend && uvicorn app.main:app --host 0.0.0.0 --port 8000"]

23
bastian/client.crt Normal file
View File

@ -0,0 +1,23 @@
Bag Attributes
friendlyName: lyman
localKeyID: E6 D3 1F DC A4 9D 66 39 16 EF D1 CB EF 93 38 32 04 2B CE 2D
subject=CN = lyman
issuer=CN = Bastion Root CA, O = Bastion WebUI, C = CN
-----BEGIN CERTIFICATE-----
MIICzzCCAnWgAwIBAgIBBzAKBggqhkjOPQQDAjA/MRgwFgYDVQQDDA9CYXN0aW9u
IFJvb3QgQ0ExFjAUBgNVBAoMDUJhc3Rpb24gV2ViVUkxCzAJBgNVBAYTAkNOMB4X
DTI1MTAyNzEzNDEzN1oXDTI2MTAyNzEzNDEzN1owEDEOMAwGA1UEAwwFbHltYW4w
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxhcnS/u1xu8s8/clsjTGX
PzbL7o/QPwDF8UY/4CpCLSA2fY32cq3bFS4qez+npdj2Nw+/+mWrewZo+qJwqkwk
qpAR2Mqxm6vZw7q8uXnM+aYyCnpHxy2QZz55+Ro0RP/HSRcBOtw98YGa2GWIy9cZ
UMgYovU8JhHBmdXs8WNntIwKWmEwXoAbQ64rqWrceuaam9BH1FzcHbrQFdKbYW1b
wt67VeQ8B6B1ZCQ038mV3DdAIsJ60YTCN3yEilD0EIvtJ7mQJCoMNfRwcNGy1eX8
KCKqyhhpCp9sAvVvAsU7lR8pY+E8FbNTMhwATT43Y01G5lUE996OSjJYnkCW1XHB
AgMBAAGjgcUwgcIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBaAwMwYJYIZI
AYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0aWZpY2F0ZTAd
BgNVHQ4EFgQUJDeldPz+ebTsSLc7adRbf0j7kBswHwYDVR0jBBgwFoAU7en/aolT
LTt2LpEBIYMcIWxJ//owDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUF
BwMCBggrBgEFBQcDBDAKBggqhkjOPQQDAgNIADBFAiEAhC2pz7QFPcTHgi2FBGXj
zDWwbkSGQXvN2EsVmuhL5XMCIBskor+/7TfJuU0YSvuCO7nD92Ys3qlH5rfL1PSE
4dba
-----END CERTIFICATE-----

32
bastian/client.key Normal file
View File

@ -0,0 +1,32 @@
Bag Attributes
friendlyName: lyman
localKeyID: E6 D3 1F DC A4 9D 66 39 16 EF D1 CB EF 93 38 32 04 2B CE 2D
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCxhcnS/u1xu8s8
/clsjTGXPzbL7o/QPwDF8UY/4CpCLSA2fY32cq3bFS4qez+npdj2Nw+/+mWrewZo
+qJwqkwkqpAR2Mqxm6vZw7q8uXnM+aYyCnpHxy2QZz55+Ro0RP/HSRcBOtw98YGa
2GWIy9cZUMgYovU8JhHBmdXs8WNntIwKWmEwXoAbQ64rqWrceuaam9BH1FzcHbrQ
FdKbYW1bwt67VeQ8B6B1ZCQ038mV3DdAIsJ60YTCN3yEilD0EIvtJ7mQJCoMNfRw
cNGy1eX8KCKqyhhpCp9sAvVvAsU7lR8pY+E8FbNTMhwATT43Y01G5lUE996OSjJY
nkCW1XHBAgMBAAECggEABydWqi/6Sk4jUGRt5qKRdCXOBogzvNRJyrrlxhruz67L
w0+1BfCE3kUIxgQ5slQsotkLmM9FY9Rv3juJtSZ9rfAUIRimVrjTgzw1P8KYfI4L
uGzuP3XQrvF5exboALbOQ7NSWqBgN0b5SQUYTAZigdYHfDZWkXtmFYdxGhdiS7pc
P31RroXLxyWpwKHmuxtfp4r8Hz3hSGEWSb978hTyWL7jkXftcICirxMk8ehC/WU9
KaDNfk6E1BjaA8p/qM5H52iGVwYWIxDgkh2w00ntL1mpG7YxoHyrZqPE0pIaMd6m
8kTUlcjrGbPlO1mUMcvZ2wezC5JHKSTmZwdLjQ/LmQKBgQDvr3zbOFZxyojiN2Nc
PCp6k3zkUHr8DoEaaRzyeIaCgzmEjZrFfmeS9OGdKV/51Dy5CxALuCUjuWQPQ+sw
GpgHTFr7lLGlgigGN87lBJrm8CnI+g0ejJzOZAACWhONipnynd7D7NX+ziR7JtgR
bUozhQZzuOpwSaJpuUSp1XbmOQKBgQC9mx1B3x8X2UWNUWaUbWfDC/dUBQRGfRR3
QNzVqSwhlUT4jA6EVLHFB/jpMbY53DOH1TdgLNoP/nsZusH9OY4IiqxdvNnWn738
mbo7hcvlTdnuZXF71SuoUCSCOaTACVXsKv6uNA/65gtxB2QFdU/yYIjrIT+wYIKW
sBvKGqgnyQKBgAebajsK7rNt2ipT17N1tWNuiug0JbMaQr6z11dau+oogArU87SQ
7nibjQ2P4pvrQIIe98NndMZNe/+ACFbegTS6F6kkbv7xwpNv8gESxFfQB4N5bDEs
BU4Hnnh0o2o6m+g3Wnqdaa1MnZvK/9CNx20bK7lAhTBLJfx1BNjfDYcBAoGBAIbA
o/V5zo5Tg2PhM0dPzgvICFo1SomSQaZTed974PppLOB8IaEY1FLUzKlnBDxw7Eqg
VT/MAJqXYQOzQEVozzHw5HmmSyeG1i6dTscY2wU35CfS/ulkYie39Yp7z0QQHnm/
QMusAqNtNTp6ZzKd4li/FPAO7EW9AXJ47PchJNtJAoGAfYphGlDnBniUYoK4PmfP
Q0kw7EQcFC/kzFSpTNmew0Nc8cymWaglesd92GXfbYfphRB89oj8/P+27eBJ8cT5
QQH4HoFypCPN1skBovRnW85boXnNXI0O/sIt7GUGX8yl3cT2/St+dKcM24KO3o+4
6h8+pyp4K79FKaYJtftptWU=
-----END PRIVATE KEY-----

View File

@ -63,19 +63,15 @@ if [ -z "$PW_SERVICE_ID" ] || [ -z "$PW_SERVER_ADDRS" ] || [ -z "$PW_LOCAL_PORT"
exit 1 exit 1
fi fi
# 拒绝 http://;要求 https://;且不允许包含路径 /portwarden客户端会自动添加 # 验证格式:支持 http:// 和 https://,但不允许包含路径 /portwarden
if echo "$PW_SERVER_ADDRS" | grep -q "http://"; then
echo "Error: PW_SERVER_ADDRS 中包含 http:// ,仅支持 https:// 地址。" >&2
exit 1
fi
if ! echo "$PW_SERVER_ADDRS" | grep -q "https://"; then
echo "Error: PW_SERVER_ADDRS 必须全部为 https:// 地址,多个地址用逗号分隔。" >&2
exit 1
fi
if echo "$PW_SERVER_ADDRS" | grep -qi "/portwarden"; then if echo "$PW_SERVER_ADDRS" | grep -qi "/portwarden"; then
echo "Error: PW_SERVER_ADDRS 不应包含路径 '/portwarden';客户端会自动添加该前缀。" >&2 echo "Error: PW_SERVER_ADDRS 不应包含路径 '/portwarden';客户端会自动添加该前缀。" >&2
exit 1 exit 1
fi fi
if ! echo "$PW_SERVER_ADDRS" | grep -qE 'https?://'; then
echo "Error: PW_SERVER_ADDRS 必须为 http:// 或 https:// 地址。" >&2
exit 1
fi
if ! echo "$PW_LOCAL_PORT" | grep -Eq '^[0-9]+$'; then if ! echo "$PW_LOCAL_PORT" | grep -Eq '^[0-9]+$'; then
echo "Error: PW_LOCAL_PORT 必须是数字端口,例如 8080。" >&2 echo "Error: PW_LOCAL_PORT 必须是数字端口,例如 8080。" >&2
exit 1 exit 1

View File

@ -9,16 +9,6 @@ const nextConfig: NextConfig = {
}, },
] ]
}, },
experimental: {
turbopack: {
// Explicitly set the root to the current directory (frontend) or project root
// Since the warning says it selected the parent directory as root, but found another lockfile here.
// We can try to force it to ignore the parent one by setting root to this one?
// "root": "./"
// Actually, let's just leave it alone if I am not sure. The user just showed the log.
// It seems harmless.
}
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -86,7 +86,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -2516,7 +2515,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -2527,7 +2525,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -2583,7 +2580,6 @@
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/types": "8.51.0", "@typescript-eslint/types": "8.51.0",
@ -3089,7 +3085,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3452,7 +3447,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -4153,7 +4147,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4339,7 +4332,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -7571,7 +7563,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -7581,7 +7572,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -7594,7 +7584,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz",
"integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -8443,8 +8432,7 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",
@ -8510,7 +8498,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -8703,7 +8690,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -9143,7 +9129,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz",
"integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==", "integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -1,6 +1,6 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect, Suspense } from "react"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { SearchStock } from "@/components/search-stock" import { SearchStock } from "@/components/search-stock"
@ -25,7 +25,7 @@ import { StockChart } from "@/components/stock-chart"
import { AiDiscussionView } from "@/components/ai-discussion-view" import { AiDiscussionView } from "@/components/ai-discussion-view"
import { HistoryView } from "@/components/history-view" import { HistoryView } from "@/components/history-view"
export default function Home() { function HomeInner() {
const searchParams = useSearchParams() const searchParams = useSearchParams()
// 状态管理 // 状态管理
@ -382,3 +382,16 @@ function SearchStockWithSelection({ onSelect }: { onSelect: (c: SearchResult, s?
</Card> </Card>
) )
} }
// Suspense wrapper for useSearchParams
export default function Home() {
return (
<Suspense fallback={
<div className="flex h-screen w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
}>
<HomeInner />
</Suspense>
)
}

View File

@ -4,7 +4,7 @@ import Link from "next/link"
import { MonitorPlay } from "lucide-react" import { MonitorPlay } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { HeaderSearch } from "@/components/header-search" import { HeaderSearch } from "@/components/header-search"
import { useEffect, useState } from "react" import { useEffect, useState, Suspense } from "react"
import { import {
Select, Select,
SelectContent, SelectContent,
@ -20,7 +20,7 @@ type RecentCompany = {
last_update: string last_update: string
} }
export function NavHeader() { function NavHeaderInner() {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const searchParams = useSearchParams()
@ -144,3 +144,21 @@ export function NavHeader() {
</header> </header>
) )
} }
// Suspense wrapper for useSearchParams
export function NavHeader() {
return (
<Suspense fallback={
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 z-[60] relative">
<div className="flex h-14 items-center gap-4 px-6 lg:h-[60px] w-full px-8">
<Link className="flex items-center gap-2 font-semibold min-w-[140px]" href="/">
<MonitorPlay className="h-6 w-6" />
<span className=""> AI</span>
</Link>
</div>
</header>
}>
<NavHeaderInner />
</Suspense>
)
}

View File

@ -188,3 +188,12 @@ export async function getReports() {
console.warn("getReports() is deprecated. Use getAnalysisHistory(companyId) instead.") console.warn("getReports() is deprecated. Use getAnalysisHistory(companyId) instead.")
return [] return []
} }
/**
* API
*/
export async function getReport(reportId: number) {
const res = await fetch(`${API_BASE}/reports/${reportId}`)
if (!res.ok) throw new Error("Failed to get report")
return res.json()
}

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "FA3-Datafetch",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}