修复 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:
parent
2edb75b19a
commit
77a08f1c55
19
Dockerfile
19
Dockerfile
@ -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
23
bastian/client.crt
Normal 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
32
bastian/client.key
Normal 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-----
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -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
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "FA3-Datafetch",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user