Public Access
Clean repo docs and remove legacy Vercel sync files
This commit is contained in:
1 parent
3ea1c3f889
commit
aa90078ce2
13 files changed
+191
-385
No files matched your search
@@ -0,0 +1,183 @@
|
|||||||
|
# VYTA
|
||||||
|
|
||||||
|
**VYTA** is a self-hosted health dashboard for Garmin data. It turns activity, sleep, recovery and training signals into a private daily overview with VYTA Age, pace of aging, training load, sleep insights, push notifications and local Garmin sync.
|
||||||
|
|
||||||
|
The project is built for people who want the feel of a polished health app without sending their personal health archive to a third-party dashboard.
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
- **VYTA Age**: a dynamic healthspan-style score based on VO2max, resting heart rate, HRV, sleep, steps and training signals.
|
||||||
|
- **Healthspan view**: weekly period display, animated orb, pace of aging and contributor breakdowns.
|
||||||
|
- **Today view**: Garmin-based recovery, strain, activity, sleep and energy summaries.
|
||||||
|
- **Sleep view**: sleep score, duration, sleep debt and sleep phase details.
|
||||||
|
- **Training view**: daily load, activity list, activity details and trend charts.
|
||||||
|
- **Notifications**: optional push alerts for recovery, HRV deviation, sleep, new activities, VYTA Age changes and more.
|
||||||
|
- **Garmin account setup in app**: each installation can connect its own Garmin account from settings.
|
||||||
|
- **Private local data**: runtime data, Garmin credentials and token stores are kept outside Git.
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/vyta/ Next.js web app
|
||||||
|
sync/vyta-sync/ Python Garmin collector, normalizer and scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime data belongs outside the repository, normally in `/data` inside the containers and mounted from your server, for example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/tank/appdata/vyta/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Privacy Model
|
||||||
|
|
||||||
|
This repo intentionally does **not** include personal data or secrets.
|
||||||
|
|
||||||
|
Never commit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
data/
|
||||||
|
private-data/
|
||||||
|
public/data/
|
||||||
|
garmin-credentials.json
|
||||||
|
.garmin_tokens/
|
||||||
|
vyta-garmin.json
|
||||||
|
vyta-data.json
|
||||||
|
settings.json
|
||||||
|
push-subscriptions.json
|
||||||
|
notification-state.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Garmin credentials are saved by the app to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/garmin-credentials.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Garmin session tokens are saved by the sync service to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/.garmin_tokens/
|
||||||
|
```
|
||||||
|
|
||||||
|
Both paths should be Docker volumes, not repository files.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The web container expects environment variables for authentication, public URL, push notifications and internal sync communication.
|
||||||
|
|
||||||
|
Common web variables:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VYTA_PUBLIC_URL
|
||||||
|
VYTA_USERNAME
|
||||||
|
VYTA_PASSWORD_HASH
|
||||||
|
VYTA_SESSION_SECRET
|
||||||
|
VYTA_DATA_PATH=/data/vyta-data.json
|
||||||
|
VYTA_SETTINGS_PATH=/data/settings.json
|
||||||
|
VYTA_PUSH_SUBSCRIPTIONS_PATH=/data/push-subscriptions.json
|
||||||
|
VYTA_SYNC_STATUS_PATH=/data/sync-status.json
|
||||||
|
VYTA_SYNC_REQUEST_PATH=/data/sync-request.json
|
||||||
|
VYTA_INTERNAL_SYNC_SECRET
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY
|
||||||
|
VAPID_PRIVATE_KEY
|
||||||
|
VAPID_SUBJECT
|
||||||
|
```
|
||||||
|
|
||||||
|
Common sync variables:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VYTA_WEB_URL
|
||||||
|
VYTA_INTERNAL_SYNC_SECRET
|
||||||
|
VYTA_OUTPUT_PATH=/data/vyta-data.json
|
||||||
|
VYTA_SYNC_INTERVAL_SECONDS=300
|
||||||
|
VYTA_SYNC_POLL_SECONDS=2
|
||||||
|
VYTA_SYNC_STATUS_PATH=/data/sync-status.json
|
||||||
|
VYTA_SYNC_REQUEST_PATH=/data/sync-request.json
|
||||||
|
VYTA_GARMIN_CREDENTIALS_PATH=/data/garmin-credentials.json
|
||||||
|
VYTA_GARMIN_TOKENSTORE=/data/.garmin_tokens
|
||||||
|
TZ=Europe/Stockholm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Web app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t vyta-web:latest app/vyta
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t vyta-sync:latest sync/vyta-sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Example
|
||||||
|
|
||||||
|
Web:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name vyta \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3777:3000 \
|
||||||
|
-v /mnt/tank/appdata/vyta/data:/data \
|
||||||
|
--env-file /path/to/vyta.env \
|
||||||
|
vyta-web:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name vyta-sync \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-v /mnt/tank/appdata/vyta/data:/data \
|
||||||
|
--env-file /path/to/vyta-sync.env \
|
||||||
|
vyta-sync:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Garmin Setup
|
||||||
|
|
||||||
|
1. Start the web app.
|
||||||
|
2. Sign in to VYTA.
|
||||||
|
3. Open **Inställningar**.
|
||||||
|
4. Connect Garmin under **Garmin-konto**.
|
||||||
|
5. The app stores credentials in `/data/garmin-credentials.json` and triggers sync.
|
||||||
|
6. The sync service uses `/data/.garmin_tokens/` after the first successful login.
|
||||||
|
|
||||||
|
Each clone or deployment can connect its own Garmin account without changing the code.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Web app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app/vyta
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd sync/vyta-sync
|
||||||
|
python -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python sync.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- Language selection in settings: Swedish and English UI.
|
||||||
|
- More install-friendly Docker Compose template.
|
||||||
|
- Better onboarding for first-time Garmin setup.
|
||||||
|
- Optional multi-user support.
|
||||||
|
- More Healthspan contributor tuning as more Garmin signals become available.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
VYTA is a personal, self-hosted health app. It is not a medical device and does not provide clinical advice.
|
||||||
+2
-35
@@ -1,36 +1,3 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# VYTA Web App
|
||||||
|
|
||||||
## Getting Started
|
Next.js frontend for VYTA. See the repository root README for setup, privacy notes and deployment instructions.
|
||||||
|
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
||||||
Generated
-72
@@ -8,7 +8,6 @@
|
|||||||
"name": "vyta",
|
"name": "vyta",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/blob": "^2.8.0",
|
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"jose": "^6.2.8",
|
"jose": "^6.2.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -2333,77 +2332,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/blob": {
|
|
||||||
"version": "2.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/blob/-/blob-2.8.0.tgz",
|
|
||||||
"integrity": "sha512-Nu+HWKpkgovCh/ezlG7wCVwF7RErTzLzZMbGKFBdGBCbTKyK+s5VXPLl+0+TpNEQPH8AVaGzOpIsXUOtkqylCQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@vercel/oidc": "^3.6.1",
|
|
||||||
"async-retry": "^1.3.3",
|
|
||||||
"is-buffer": "^2.0.5",
|
|
||||||
"is-node-process": "^1.2.0",
|
|
||||||
"throttleit": "^2.1.0",
|
|
||||||
"undici": "^6.23.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/cli-config": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-Ggh0Wmi92TUkUexmSUPkkDtvJmbjUr7IvF5T3FkSsWrXXs3GFzOujfxFpECdJZpux1JG4SWDv9BT4w++TDgD6A==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"xdg-app-paths": "5",
|
|
||||||
"zod": "4.1.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/cli-config/node_modules/zod": {
|
|
||||||
"version": "4.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz",
|
|
||||||
"integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/cli-exec": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/cli-exec/-/cli-exec-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"execa": "5.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/oidc": {
|
|
||||||
"version": "3.8.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.8.4.tgz",
|
|
||||||
"integrity": "sha512-FGNvVZ5pgX9FaBqkPt6VkYFZ6bWAMDzYi7nxW+1Xt+Z4fn5PuTULVwsxjKc+0uKhysyWBQmvsmM50Oh6C2/oMA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@vercel/cli-config": "0.2.3",
|
|
||||||
"@vercel/cli-exec": "1.0.1",
|
|
||||||
"jose": "^5.9.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vercel/oidc/node_modules/jose": {
|
|
||||||
"version": "5.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
|
||||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/panva"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/blob": "^2.8.0",
|
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"jose": "^6.2.8",
|
"jose": "^6.2.8",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export async function GET(
|
|||||||
const publicUrl =
|
const publicUrl =
|
||||||
process.env
|
process.env
|
||||||
.VYTA_PUBLIC_URL ||
|
.VYTA_PUBLIC_URL ||
|
||||||
"https://vyta.bitli.se";
|
"http://localhost:3000";
|
||||||
|
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(
|
new URL(
|
||||||
@@ -181,3 +181,4 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Executable → Regular
File mode changed.
@@ -15,7 +15,7 @@ export async function POST() {
|
|||||||
new URL(
|
new URL(
|
||||||
"/login",
|
"/login",
|
||||||
publicUrl ||
|
publicUrl ||
|
||||||
"https://vyta.bitli.se"
|
"http://localhost:3000"
|
||||||
),
|
),
|
||||||
303
|
303
|
||||||
);
|
);
|
||||||
@@ -45,3 +45,5 @@ export async function POST() {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Executable → Regular
File mode changed.
Executable → Regular
File mode changed.
@@ -1,8 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
cd /d "C:\Users\Silicon Solder\Documents\Codex\vyta-sync"
|
|
||||||
|
|
||||||
"C:\Users\Silicon Solder\Documents\Codex\vyta-sync\.venv\Scripts\python.exe" ^
|
|
||||||
"C:\Users\Silicon Solder\Documents\Codex\vyta-sync\sync.py"
|
|
||||||
|
|
||||||
exit /b %errorlevel%
|
|
||||||
+1
-31
@@ -21,10 +21,6 @@ NORMALIZER = os.path.join(
|
|||||||
"normalizer.py",
|
"normalizer.py",
|
||||||
)
|
)
|
||||||
|
|
||||||
UPLOADER = os.path.join(
|
|
||||||
BASE_DIR,
|
|
||||||
"upload_to_blob.py",
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG_DIR = os.path.join(
|
LOG_DIR = os.path.join(
|
||||||
BASE_DIR,
|
BASE_DIR,
|
||||||
@@ -179,24 +175,6 @@ def run_script(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def should_upload_to_blob():
|
|
||||||
value = os.environ.get(
|
|
||||||
"VYTA_UPLOAD_TO_BLOB",
|
|
||||||
"0",
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
value
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
in (
|
|
||||||
"1",
|
|
||||||
"true",
|
|
||||||
"yes",
|
|
||||||
"on",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def notify_vyta():
|
def notify_vyta():
|
||||||
@@ -312,15 +290,6 @@ def main():
|
|||||||
NORMALIZER
|
NORMALIZER
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_upload_to_blob():
|
|
||||||
run_script(
|
|
||||||
UPLOADER
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log(
|
|
||||||
"Hoppar över Vercel Blob-upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
notify_vyta()
|
notify_vyta()
|
||||||
|
|
||||||
completed_at = utc_now()
|
completed_at = utc_now()
|
||||||
@@ -377,3 +346,4 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import date
|
|
||||||
from getpass import getpass
|
|
||||||
|
|
||||||
from garminconnect import Garmin
|
|
||||||
|
|
||||||
|
|
||||||
TOKENSTORE = os.path.join(os.path.dirname(__file__), ".garmin_tokens")
|
|
||||||
|
|
||||||
|
|
||||||
def dump(title, data):
|
|
||||||
print()
|
|
||||||
print("=" * 70)
|
|
||||||
print(title)
|
|
||||||
print("=" * 70)
|
|
||||||
print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
email = input("Garmin email: ").strip()
|
|
||||||
password = getpass("Garmin password: ")
|
|
||||||
|
|
||||||
garmin = Garmin(
|
|
||||||
email=email,
|
|
||||||
password=password,
|
|
||||||
)
|
|
||||||
|
|
||||||
os.makedirs(TOKENSTORE, exist_ok=True)
|
|
||||||
|
|
||||||
print("\nLoggar in mot Garmin Connect...")
|
|
||||||
|
|
||||||
result = garmin.login(TOKENSTORE)
|
|
||||||
|
|
||||||
print("Inloggning OK.")
|
|
||||||
print("Display name:", garmin.display_name)
|
|
||||||
|
|
||||||
today = date.today().isoformat()
|
|
||||||
|
|
||||||
print(f"\nHämtar Garmin-data för {today}...")
|
|
||||||
|
|
||||||
calls = [
|
|
||||||
("Daily stats", lambda: garmin.get_stats(today)),
|
|
||||||
("Heart rate", lambda: garmin.get_heart_rates(today)),
|
|
||||||
("Sleep", lambda: garmin.get_sleep_data(today)),
|
|
||||||
("HRV", lambda: garmin.get_hrv_data(today)),
|
|
||||||
("Stress", lambda: garmin.get_stress_data(today)),
|
|
||||||
("Body Battery", lambda: garmin.get_body_battery(today)),
|
|
||||||
("Respiration", lambda: garmin.get_respiration_data(today)),
|
|
||||||
("SpO2", lambda: garmin.get_spo2_data(today)),
|
|
||||||
("Training readiness", lambda: garmin.get_training_readiness(today)),
|
|
||||||
("Training status", lambda: garmin.get_training_status(today)),
|
|
||||||
("Activities", lambda: garmin.get_activities(0, 10)),
|
|
||||||
("Devices", lambda: garmin.get_devices()),
|
|
||||||
]
|
|
||||||
|
|
||||||
for title, fn in calls:
|
|
||||||
try:
|
|
||||||
data = fn()
|
|
||||||
dump(title, data)
|
|
||||||
except Exception as e:
|
|
||||||
print()
|
|
||||||
print("=" * 70)
|
|
||||||
print(title)
|
|
||||||
print("=" * 70)
|
|
||||||
print(f"ERROR: {type(e).__name__}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# CONFIG
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
VYTA_PROJECT = Path(
|
|
||||||
r"C:\Users\Silicon Solder\Documents\Codex\vyta"
|
|
||||||
)
|
|
||||||
|
|
||||||
DATA_FILE = (
|
|
||||||
VYTA_PROJECT
|
|
||||||
/ "private-data"
|
|
||||||
/ "vyta-data.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
BLOB_PATH = "vyta/vyta-data.json"
|
|
||||||
|
|
||||||
|
|
||||||
# Läs vyta-sync\.env
|
|
||||||
load_dotenv(SCRIPT_DIR / ".env")
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# HELPERS
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
def find_vercel_cli() -> str:
|
|
||||||
"""
|
|
||||||
Hitta Vercel CLI.
|
|
||||||
|
|
||||||
På Windows ligger global npm CLI normalt som vercel.cmd.
|
|
||||||
"""
|
|
||||||
|
|
||||||
candidates = [
|
|
||||||
"vercel.cmd",
|
|
||||||
"vercel",
|
|
||||||
]
|
|
||||||
|
|
||||||
for candidate in candidates:
|
|
||||||
executable = shutil.which(candidate)
|
|
||||||
|
|
||||||
if executable:
|
|
||||||
return executable
|
|
||||||
|
|
||||||
# Vanlig npm global-bin på Windows
|
|
||||||
appdata = os.environ.get("APPDATA")
|
|
||||||
|
|
||||||
if appdata:
|
|
||||||
fallback = Path(appdata) / "npm" / "vercel.cmd"
|
|
||||||
|
|
||||||
if fallback.exists():
|
|
||||||
return str(fallback)
|
|
||||||
|
|
||||||
raise FileNotFoundError(
|
|
||||||
"Kunde inte hitta Vercel CLI. "
|
|
||||||
"Kontrollera att 'vercel --version' fungerar i PowerShell."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("VYTA → VERCEL PRIVATE BLOB")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Kontrollera datafil
|
|
||||||
# --------------------------------------------------------
|
|
||||||
|
|
||||||
if not DATA_FILE.exists():
|
|
||||||
print()
|
|
||||||
print("ERROR: Vyta-datafilen saknas:")
|
|
||||||
print(DATA_FILE)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"Datafil: {DATA_FILE}")
|
|
||||||
print(f"Storlek: {DATA_FILE.stat().st_size:,} bytes")
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Blob-token
|
|
||||||
# --------------------------------------------------------
|
|
||||||
|
|
||||||
token = os.environ.get(
|
|
||||||
"BLOB_READ_WRITE_TOKEN"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
"ERROR: BLOB_READ_WRITE_TOKEN saknas i vyta-sync\\.env"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("Blob-token: OK")
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Vercel CLI
|
|
||||||
# --------------------------------------------------------
|
|
||||||
|
|
||||||
try:
|
|
||||||
vercel_cli = find_vercel_cli()
|
|
||||||
except FileNotFoundError as exc:
|
|
||||||
print()
|
|
||||||
print(f"ERROR: {exc}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"Vercel CLI: {vercel_cli}")
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Upload
|
|
||||||
# --------------------------------------------------------
|
|
||||||
|
|
||||||
command = [
|
|
||||||
vercel_cli,
|
|
||||||
"blob",
|
|
||||||
"put",
|
|
||||||
str(DATA_FILE),
|
|
||||||
"--pathname",
|
|
||||||
BLOB_PATH,
|
|
||||||
"--access",
|
|
||||||
"private",
|
|
||||||
"--allow-overwrite",
|
|
||||||
"--content-type",
|
|
||||||
"application/json",
|
|
||||||
"--rw-token",
|
|
||||||
token,
|
|
||||||
]
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
f"Laddar upp till Private Blob: {BLOB_PATH}"
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
command,
|
|
||||||
cwd=str(VYTA_PROJECT),
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
f"ERROR: Blob-upload misslyckades "
|
|
||||||
f"(exit code {result.returncode})"
|
|
||||||
)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("=" * 60)
|
|
||||||
print("VYTA BLOB-UPLOAD KLAR")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in new issue
Block a user