diff --git a/.gitignore b/.gitignore index a38ebeb..c79010a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ .DS_Store .vercel .claude/ +.env* diff --git a/api/state.js b/api/state.js new file mode 100644 index 0000000..25f5b46 --- /dev/null +++ b/api/state.js @@ -0,0 +1,44 @@ +import { get, put } from "@vercel/blob"; + +const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json"; + +async function readJsonBlob() { + const blob = await get(STATE_PATH, { access: "private" }); + if (!blob || blob.statusCode !== 200 || !blob.stream) return null; + const text = await new Response(blob.stream).text(); + return text ? JSON.parse(text) : null; +} + +export default async function handler(req, res) { + res.setHeader("Cache-Control", "no-store"); + try { + if (!process.env.BLOB_READ_WRITE_TOKEN && !process.env.VERCEL_OIDC_TOKEN) { + return res.status(503).json({ ok: false, error: "Cloud state is not configured" }); + } + + if (req.method === "GET") { + return res.status(200).json({ ok: true, state: await readJsonBlob() }); + } + + if (req.method === "POST") { + const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {}); + if (!body || typeof body !== "object" || !body.items || typeof body.items !== "object") { + return res.status(400).json({ ok: false, error: "Invalid state payload" }); + } + const state = { version: 1, updated_at: new Date().toISOString(), items: body.items }; + await put(STATE_PATH, JSON.stringify(state), { + access: "private", + allowOverwrite: true, + contentType: "application/json", + cacheControlMaxAge: 60, + }); + return res.status(200).json({ ok: true, state }); + } + + res.setHeader("Allow", "GET, POST"); + return res.status(405).json({ ok: false, error: "Method not allowed" }); + } catch (err) { + const message = err && err.message ? err.message : "State sync failed"; + return res.status(500).json({ ok: false, error: message }); + } +} diff --git a/index.html b/index.html index 8b1b802..069a178 100644 --- a/index.html +++ b/index.html @@ -381,9 +381,9 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color

Device sync

-

Portfolios, returns, reports, suggestions, and chart history are saved in this browser. To make another device match, click Copy Sync here, then open the site on the other device and use Import Sync.

+

Portfolios, returns, reports, suggestions, and chart history now sync through the site automatically. Copy Sync and Import Sync remain as a fallback for moving the exact state between devices.

-
⚠️ Paper trading only — not financial advice. Nothing here places real orders or moves money. The analysis is a transparent heuristic. All portfolios live in this browser's local storage (each device keeps its own).
+
⚠️ Paper trading only — not financial advice. Nothing here places real orders or moves money. The analysis is a transparent heuristic. Portfolios sync through the site when cloud state is available, with this browser keeping a local backup.