Skip to content
IDEA-AmritaPublic

About

Personalised Nutrition and Workout Recommendation Engine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation


Hostel students can't choose their ingredients. The mess serves what it serves, so generic calorie apps, built around cooking your own food, don't fit. MessFit starts from today's mess menu and works out how much of each dish to take to hit your calorie and macro targets, within your allergies, diet and health conditions. Then it helps you log it, train, track progress, and ask a coach that answers from vetted sources.

Built for hostellers at Amrita Vishwa Vidyapeetham, Coimbatore, by the IDEA Club.

By the numbers: 591 backend tests run under row-level security, 103 end-to-end tests, 47 API endpoints across 11 routers, row-level security on all 23 tables, 19 reversible migrations, 50 optimizer scenarios evaluated in CI.

Screens

Home: today's plan, macros, streak
Today: plan, macros, streak
Plate optimizer with per-dish portions
Plate: portions from the live menu
Daily log check-in
Log: one-tap daily check-in
Weight trend and adherence
Progress: trend, adherence, projection
AI coach answer with citations
Coach: grounded answers with citations
Workout session with set tracking
Workout: sets, rest timer, progression
Desktop
Desktop home dashboard Desktop plate optimizer

Screens use demo data and are generated by apps/web/scripts/readme-shots.

Features

Plate optimizer

A mixed-integer linear program (PuLP / CBC) picks portions from today's menu at your mess to land on your calorie and macro targets, within your allergens, diet and conditions. Every dish gets a one-line reason; canteen add-ons close any gap that's left. A photo of the menu board works when it differs from the app.

Goal engine

Mifflin-St Jeor BMR → activity-adjusted TDEE → calorie and macro targets from your profile, goal and rate. As weight logs arrive, an adaptive TDEE estimate corrects the formula with your real data.

AI coach

Retrieval-augmented chat grounded in MessFit's own articles (Gemini embeddings + pgvector), streamed over SSE. It cites sources, refuses medical questions and caches common answers. Groq is the fallback model.

Logging and progress

One-tap meal check-ins, weight, mood and energy. Weight trend, adherence, macro-hit rate, streak badges and a projected date for reaching your goal.

Workouts

Hostel-friendly templates, from bodyweight in your room to the gym, with week-by-week progression, set tracking, a rest timer and form cues.

Menu OCR and admin

Admins photograph the weekly menu board; a worker reads it with a vision model and an admin reviews it before anything goes live. Plus dish and mess management and opt-out analytics.

Learn

15 public articles on nutrition and training, written for hostel life. The coach's citations link straight to them.

Installable PWA

Runs on any phone without an app store: installable, offline-aware, Web Push for the weekly check-in, a command palette and a mobile-first motion system.

System design

Architecture

MessFit architecture: a Next.js PWA and admin console call a FastAPI modular monolith, which uses Postgres on Supabase with row-level security, Redis, Gemini and Groq, Supabase Storage and Web Push; Celery workers run OCR and scheduled jobs; OpenTelemetry and Sentry provide observability.

A modular monolith: one FastAPI service with a package per domain, one async SQLAlchemy engine, and Postgres as the single store for relational data and embeddings. Every request carries the user's identity into Postgres, so row-level security enforces isolation in the database itself, not only in the API. The plate optimizer runs in the request with a Redis result cache; menu OCR and scheduled jobs go through Redis to Celery. Design notes: technical design · schema · decision records.

Deployment view
flowchart TB
    subgraph Client["Student's phone / laptop"]
        PWA["MessFit PWA<br/>service worker"]
    end
    subgraph App["Application tier"]
        WEB["Next.js server<br/>SSR + auth proxy"]
        API["FastAPI<br/>uvicorn workers"]
        WRK["Celery worker"]
        BEAT["Celery beat"]
    end
    subgraph Supabase["Supabase (managed)"]
        AUTH["Auth · JWKS"]
        PG[("Postgres 16<br/>RLS · pgvector")]
        ST[("Storage<br/>menu photos")]
    end
    REDIS[("Redis<br/>broker · cache · limits")]
    LLM["Gemini · Groq"]
    PUSH["Push services<br/>FCM · Mozilla · Apple · WNS"]
    OBS["OTLP collector → Grafana<br/>Sentry"]

    PWA --> WEB
    PWA -- "REST + SSE, Bearer JWT" --> API
    PWA -- sign-in --> AUTH
    API -- JWKS --> AUTH
    API -- "messfit_app (RLS)" --> PG
    API --> REDIS
    API --> LLM
    API --> ST
    BEAT --> REDIS
    REDIS --> WRK
    WRK -- "messfit_worker" --> PG
    WRK --> LLM
    WRK --> ST
    WRK --> PUSH
    PUSH -. notification .-> PWA
    API & WRK -.-> OBS
Loading

User flows

First run: sign-up to first plate
flowchart TD
    A([Open MessFit]) --> B{Signed in?}
    B -- no --> C[Sign up or log in<br/>email · Google · GitHub]
    C --> D[Email confirmation / OAuth callback]
    D --> E{Onboarded?}
    B -- yes --> E
    E -- no --> F[1 · Profile<br/>age, sex, height, weight]
    F --> G[2 · Goal<br/>lose · maintain · gain, rate]
    G --> H[3 · Diet<br/>diet type, allergies, conditions]
    H --> I[4 · Hostel<br/>mess, canteen budget, equipment]
    I --> J[5 · Targets<br/>calories and macros, editable]
    J --> K([Today's dashboard])
    E -- yes --> K
    K --> L[Plate for today's menu]
Loading

The route proxy enforces this order: guests are sent to log in, signed-in users who haven't finished onboarding are sent to it, and /admin is checked against the database role on the server.

Daily loop
flowchart LR
    M[Morning: open Plate] --> P[Optimized plate<br/>for each meal]
    P --> V{Dish actually served?}
    V -- no --> X[Hide it on the Menu page<br/>plate re-solves without it]
    X --> P
    V -- yes --> E[Eat]
    P -. served today? vote .-> C[Crowd confirms the menu<br/>for other students]
    E --> L[Log: as planned · different · skipped]
    L --> W[Weight, mood, workout]
    W --> R[Progress: trend, adherence, streak]
    R --> T[Adaptive TDEE<br/>recalibrates targets]
    T --> M
Loading
Admin: weekly menu from a photo
flowchart LR
    U[Admin uploads<br/>menu-board photo] --> Q[Job queued]
    Q --> O[Worker: vision OCR<br/>Gemini, Groq fallback]
    O --> R[Review screen<br/>match dishes, fix names]
    R -- approve --> A[Menu rows upserted<br/>unknown dishes drafted]
    R -- reject --> J[Discarded]
    A --> S[Students' plates<br/>use the new menu]
Loading

UML

Class diagram: optimizer core (hexagonal)

The solver is a pure function over frozen dataclasses. HTTP, cache and Celery adapters sit around it, which is why 50 evaluation scenarios run with no infrastructure at all.

classDiagram
    direction LR
    class OptimizationInput {
        <<frozen>>
        +float daily_kcal
        +float daily_protein_g
        +float daily_carbs_g
        +float daily_fats_g
        +str diet_type
        +tuple allergies
        +tuple conditions
        +str goal
        +dict menu
        +tuple canteen_items
        +int canteen_budget_inr
        +tuple skip_dish_ids
    }
    class Dish {
        <<frozen>>
        +str id
        +str name
        +str diet_type
        +str serving_unit
        +float kcal
        +float protein_g
        +tuple allergens
        +is_integer_only() bool
    }
    class CanteenItem {
        <<frozen>>
        +str id
        +str name
        +int cost_inr
        +float kcal
        +float protein_g
    }
    class OptimizationOutput {
        +dict plan
        +dict daily_totals
        +dict daily_targets
        +list gap_fills
        +str solver_status
        +int solve_time_ms
    }
    class PlateItem {
        +str dish_id
        +float portions
        +float grams
        +float kcal
        +str reason
    }
    class GapFill {
        +str item_id
        +float portions
        +int cost_inr
        +str text
    }
    class Solver {
        <<service>>
        +optimize(OptimizationInput) OptimizationOutput
        +is_eligible(Dish, OptimizationInput) bool
        +portion_cap(Dish, conditions) int
    }
    class Reasons {
        <<service>>
        +annotate(OptimizationOutput, OptimizationInput)
    }
    class ResultCache {
        <<adapter>>
        +get_or_optimize(redis, OptimizationInput) OptimizationOutput
    }
    class OptimizeRoutes {
        <<adapter>>
        +optimize_today()
        +optimize_photo()
    }
    OptimizationInput "1" *-- "many" Dish
    OptimizationInput "1" *-- "many" CanteenItem
    OptimizationOutput "1" *-- "many" PlateItem
    OptimizationOutput "1" *-- "many" GapFill
    Solver ..> OptimizationInput : reads
    Solver ..> OptimizationOutput : creates
    Reasons ..> OptimizationOutput : explains
    ResultCache --> Solver : on cache miss
    OptimizeRoutes --> ResultCache
Loading
Sequence: an authenticated request under row-level security
sequenceDiagram
    autonumber
    participant B as Browser
    participant A as FastAPI
    participant K as Supabase JWKS
    participant P as Postgres (messfit_app)
    B->>A: GET /api/v1/logs/today · Bearer JWT
    A->>A: rate limit (per route)
    A->>K: public keys (cached for 1 h)
    A->>A: verify ES256 signature and expiry
    A->>P: set_config('request.jwt.claims', {sub})
    A->>P: SELECT … FROM meal_logs
    Note over P: RLS policy: user_id = auth.uid()<br/>other students' rows are invisible
    P-->>A: only this student's rows
    A->>P: RESET request.jwt.claims
    A-->>B: 200 JSON
Loading
Sequence: building today's plate
sequenceDiagram
    autonumber
    participant B as Browser
    participant A as Optimizer route
    participant P as Postgres
    participant R as Redis
    participant S as MILP solver
    B->>A: POST /api/v1/optimize/today
    A->>P: profile, targets, today's menu, exclusions
    A->>A: build OptimizationInput
    A->>R: GET hash(input)
    alt cache hit
        R-->>A: cached plate
    else miss (or Redis down)
        A->>S: optimize(input)
        S-->>A: plate + totals
        A->>R: SETEX hash(input)
    end
    A->>A: annotate reasons, gap fills
    A-->>B: plan · totals · targets · solve time
Loading
Sequence: how the coach answers
sequenceDiagram
    autonumber
    participant U as Student
    participant API as FastAPI
    participant DB as Postgres + pgvector
    participant LLM as Gemini (Groq fallback)
    U->>API: question (Bearer JWT, rate-limited)
    API->>LLM: embed the question
    API->>DB: semantic cache lookup (non-personal questions)
    API->>DB: nearest article chunks
    API->>API: medical-advice pre-check
    API->>LLM: grounded prompt
    LLM-->>API: tokens
    API-->>U: SSE stream + citations
    API->>DB: save message, cache answer
Loading
State machines: OCR job and account lifecycle
stateDiagram-v2
    direction LR
    [*] --> pending: admin uploads photo
    pending --> processing: worker picks up
    processing --> ready_for_review: parsed and validated
    processing --> failed: both models failed
    ready_for_review --> approved: admin approves
    ready_for_review --> rejected: admin rejects
    failed --> approved: admin enters menu by hand
    failed --> rejected
    approved --> [*]
    rejected --> [*]
Loading
stateDiagram-v2
    direction LR
    [*] --> active: sign-up
    active --> onboarded: finishes onboarding
    onboarded --> pending_deletion: deletes account (access blocked)
    pending_deletion --> erased: nightly sweep after 30 days
    erased --> [*]
Loading

Database design

Postgres 16 on Supabase, managed by 19 reversible Alembic migrations. Conventions:

  • Row-level security on all 23 tables. Personal rows are visible only to their owner (user_id = auth.uid()); shared catalogues (dishes, menus, articles) are readable by any signed-in user and writable only by admins.
  • Erasure by cascade. Every personal table references users with ON DELETE CASCADE, so deleting a user erases all of their data in one statement.
  • Natural keys where the domain has them: one weight per user per day, one vote per dish per meal, one menu row per mess, date, meal and dish.
  • Enumerations as CHECK constraints (for example meal_type, diet_type, ocr_jobs.status).
  • Embeddings in-database: kb_chunks.embedding and chat_cache.query_embedding are pgvector columns; dish search uses pg_trgm.
1 · Identity, profile and settings
erDiagram
    users ||--o| profiles : "has"
    users ||--o| hostel_contexts : "lives in"
    messes ||--o{ hostel_contexts : "serves"
    users ||--o| notification_preferences : "sets"
    users ||--o{ push_subscriptions : "registers"

    users {
        uuid id PK "= auth.users.id"
        text email UK
        text display_name
        text role "user | admin"
        timestamptz onboarded_at
        timestamptz deleted_at "soft delete"
    }
    profiles {
        uuid user_id PK, FK
        date dob
        text sex
        numeric height_cm
        numeric current_weight_kg
        numeric target_weight_kg
        numeric target_rate_kg_per_week
        text goal "lose | maintain | gain"
        int activity_level
        text diet_type "veg | eggetarian | non_veg | jain"
        text_array allergies
        text_array conditions
    }
    hostel_contexts {
        uuid user_id PK, FK
        uuid mess_id FK
        text canteen_freq
        int top_up_budget_inr_weekly
        text_array equipment
        int workout_days_per_week
    }
    messes {
        uuid id PK
        text name UK "with college"
        text college
        text city
    }
    notification_preferences {
        uuid user_id PK, FK
        bool weekly_checkin
    }
    push_subscriptions {
        uuid id PK
        uuid user_id FK
        text endpoint UK
        text p256dh
        text auth
    }
Loading
2 · Daily tracking and analytics
erDiagram
    users ||--o{ meal_logs : "logs"
    users ||--o{ weight_logs : "logs"
    users ||--o{ subjective_logs : "logs"
    users ||--o{ workout_logs : "logs"
    workout_templates ||--o{ workout_logs : "followed in"
    users ||--o{ analytics_events : "emits"

    meal_logs {
        uuid id PK
        uuid user_id FK
        date date UK "with user, meal"
        text meal_type
        text status "as_planned | different | skipped"
        numeric kcal
        numeric protein_g
    }
    weight_logs {
        uuid user_id PK, FK
        date date PK
        numeric weight_kg
    }
    subjective_logs {
        uuid user_id PK, FK
        date date PK
        int energy
        int hunger
        int mood
    }
    workout_logs {
        uuid id PK
        uuid user_id FK
        date date
        text template_id FK
        jsonb exercises_done
        text status "done | partial | skipped"
    }
    workout_templates {
        text id PK
        text name
        text goal
        text_array equipment_required
        jsonb structure "weeks, days, exercise ids"
    }
    analytics_events {
        bigint id PK
        uuid user_id FK
        text name
        jsonb props
        timestamptz occurred_at "pruned after 180 days"
    }
Loading
3 · Mess, menus and OCR
erDiagram
    messes ||--o{ mess_menus : "publishes"
    dishes ||--o{ mess_menus : "appears in"
    messes ||--o{ ocr_jobs : "menu photos"
    users |o--o{ ocr_jobs : "uploads"
    users ||--o{ dish_exclusions : "hides"
    dishes ||--o{ dish_exclusions : "hidden"
    users ||--o{ dish_feedback : "votes"
    dishes ||--o{ dish_feedback : "voted on"

    dishes {
        uuid id PK
        text name UK "with serving unit"
        text category
        text diet_type "vegan | veg | egg | non_veg"
        text default_serving_unit
        numeric kcal
        numeric protein_g
        numeric carbs_g
        numeric fats_g
        text_array allergens
        text portion_icon
        text confidence "verified | estimated | user_reported"
    }
    mess_menus {
        uuid id PK
        uuid mess_id FK
        uuid dish_id FK
        date effective_from
        int day_of_week
        text meal_type
        text availability "always | usually | sometimes"
    }
    ocr_jobs {
        uuid id PK
        uuid mess_id FK
        uuid uploaded_by FK
        text photo_url
        text status
        jsonb parsed_result
    }
    dish_exclusions {
        uuid user_id PK, FK
        date date PK
        text meal_type PK
        uuid dish_id PK, FK
    }
    dish_feedback {
        uuid user_id PK, FK
        date date PK
        text meal_type PK
        uuid dish_id PK, FK
        text vote "confirm | deny"
    }
Loading
4 · AI coach and knowledge base
erDiagram
    users ||--o{ chatbot_conversations : "starts"
    chatbot_conversations ||--o{ chatbot_messages : "contains"
    kb_documents ||--o{ kb_chunks : "split into"

    chatbot_conversations {
        uuid id PK
        uuid user_id FK
        text title
    }
    chatbot_messages {
        uuid id PK
        uuid conversation_id FK
        text role "user | assistant | system"
        text content
        jsonb citations
        text model
    }
    kb_documents {
        uuid id PK
        text source
        text title
    }
    kb_chunks {
        uuid id PK
        uuid document_id FK
        text content
        vector embedding
    }
    chat_cache {
        uuid id PK
        vector query_embedding
        text response
        jsonb citations
    }
Loading

chat_cache is keyed by embedding similarity, not by a foreign key, and exercises is a standalone catalogue referenced by id from workout_templates.structure. Column-level notes: docs/02-tdd/SCHEMA.md.

API reference

47 endpoints across 11 routers, all documented by OpenAPI at http://localhost:8000/docs when the API is running.

Area Prefix Highlights
Profile and targets /api/v1/profile profile, hostel context, computed targets
Optimizer /api/v1/optimize today, photo
Tracking /api/v1/logs meals, weight, subjective, workout, progress, leaderboard
Workouts /api/v1/workouts today's session, templates
Coach /api/v1/chat conversations, messages (SSE)
Notifications /api/v1/notifications subscribe, unsubscribe, preferences
Analytics /api/v1/analytics event ingest, admin summary
Account /api/v1/account DPDP deletion
Mess /mess messes, dishes, daily menu, exclusions, dish feedback
Admin /mess/admin, /mess/admin/ocr mess, dish and menu management; OCR review

Tech stack

Tech stack: Next.js 16, React 19, TypeScript, Tailwind 4, Framer Motion, PWA; Python 3.12, FastAPI, Pydantic, SQLAlchemy 2, Celery, uv; Postgres, Supabase, Redis, Gemini; pytest, Playwright, ESLint, GitHub Actions, Docker, k6; OpenTelemetry, Grafana, Prometheus, Sentry, pnpm, Node 24.
Decision Choice Record
Client Next.js PWA: one codebase, no app store ADR-004
Service shape FastAPI modular monolith ADR-007
Auth Supabase Auth, ES256 JWTs via JWKS ADR-003
Optimizer PuLP / CBC linear MILP ADR-002
Menu input Vision OCR with human review ADR-008
Repo and tooling Monorepo with uv and pnpm workspaces ADR-001 · ADR-005 · ADR-006

Quick start

Prerequisites: Node 24 LTS (pnpm 11 needs at least 22.13) with pnpm 11, Python 3.12 with uv, and Docker.

git clone https://github.com/IDEA-Amrita/Mess-Fit.git
cd Mess-Fit

# Dependencies
corepack enable && corepack prepare pnpm@11 --activate
pnpm install
cd services/api && uv sync && cd ../..

# Environment (then fill in your own keys)
cp services/api/.env.example services/api/.env
cp apps/web/.env.local.example apps/web/.env.local

# Local Postgres + Redis, then migrate
docker compose up -d
cd services/api && uv run alembic upgrade head && cd ../..

Run it in three terminals:

# 1 · API on :8000
cd services/api && uv run uvicorn messfit_api.main:app --reload --port 8000

# 2 · Web on :3000
cd apps/web && pnpm dev

# 3 · Worker + scheduler (menu OCR, scheduled jobs), optional
cd services/api && uv run celery -A messfit_api.celery_app worker --beat --loglevel=info

Or run the backend from the production image instead of terminals 1 and 3: docker compose --profile app up -d --build starts the API, worker and beat, configured by services/api/.env (see the deploy runbook).

Open http://localhost:3000. The API is healthy when curl http://localhost:8000/health returns {"status":"ok", ...}.

To load the coach's knowledge base, run uv run python scripts/ingest_articles.py from services/api; the mess, dish and workout seed scripts are alongside it.

Configuration

Secrets live in git-ignored .env files; production uses host environment variables. Full lists: services/api/.env.example and apps/web/.env.local.example.

Group Variables
Core DATABASE_URL, REDIS_URL, SUPABASE_*, GEMINI_API_KEY, GROQ_API_KEY
Observability (no-op when empty) OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, SENTRY_DSN, NEXT_PUBLIC_SENTRY_DSN
Rate limiting RATE_LIMIT_ENABLED, RATE_LIMIT_STORAGE_URI (the Redis URL in production)

Testing and quality

Every pull request runs the full pipeline in GitHub Actions:

Stage What runs
API ruff lint and format · mypy · migrations built from scratch, reversed and rebuilt · 591 pytest tests as the RLS-restricted app role, including a 401 check on every protected endpoint · 50-scenario optimizer evaluation · pip-audit
Image production container built, started and required to report healthy as a non-root user · Trivy scan fails on fixable HIGH/CRITICAL vulnerabilities
Web TypeScript · ESLint · production build · 103 Playwright end-to-end tests · pnpm audit
Load k6 scripts for the optimizer, chat and a smoke run (infra/load-tests)

The backend suite runs on its own throwaway Postgres and refuses to run against Supabase. scripts/test-db.ps1 starts one in Docker with the image CI uses:

cd services/api
./scripts/test-db.ps1                  # add -Reset for a fresh database
$env:TEST_DATABASE_URL = "postgresql+asyncpg://messfit_app:messfit_app@127.0.0.1:55432/messfit_test"
$env:TEST_CELERY_DATABASE_URL = "postgresql+asyncpg://messfit_worker:messfit_worker@127.0.0.1:55432/messfit_test"
uv run ruff check . ; uv run mypy messfit_api ; uv run pytest -q ; uv run pytest eval/ -q

cd ../../apps/web
pnpm exec tsc --noEmit ; pnpm exec eslint . ; pnpm build ; pnpm exec playwright test

Security and privacy

Control How
Data isolation Row-level security on every table; the API connects as a least-privilege role that cannot bypass it; Supabase's public Data API roles have no table access
Identity Supabase JWTs verified against the project's JWKS on every request; admin rights come from the database, and nobody can promote themselves
Input handling Image uploads allow-listed by type, capped at 10 MB and checked by magic bytes; push endpoints must be HTTPS on known push services
Abuse Per-route rate limits, Redis-backed in production
Data rights Account deletion with a 30-day grace period, then a nightly hard delete (India's DPDP Act); analytics are opt-out, honour Global Privacy Control and expire after 180 days
Supply chain pip-audit and pnpm audit on every pull request; lockfiles committed and installed frozen
Operations Runbooks for secret rotation, incident response, backup and restore, deploy and rollback and scaling

Repository layout

Mess-Fit/
├── apps/web/                  Next.js 16 PWA (student app + admin console)
│   ├── src/content/articles/  15 /learn articles, also the coach's knowledge base
│   └── tests/                 Playwright end-to-end suite
├── services/api/              FastAPI modular monolith
│   ├── messfit_api/           profile · mess · optimizer · tracking · workouts · chatbot
│   │                          notifications · analytics · account · observability
│   ├── infra/migrations/      Alembic migrations (raw SQL, RLS policies)
│   ├── scripts/               seed, ingest and test-database scripts
│   ├── eval/                  optimizer and chatbot evaluation harness
│   └── tests/                 pytest suite
├── infra/
│   ├── grafana/dashboards/    API health (RED), business KPIs, AI and optimizer
│   └── load-tests/            k6 scripts
├── docs/                      PRD, technical design, ADRs, runbooks, README assets
└── docker-compose.yml         local Postgres + Redis

Documentation

Document Covers
Product requirements Who it's for, the problem, scope
Technical design · Schema Architecture and data model
Decision records Why each major choice was made
Runbooks Backup and restore, incidents, secrets, deploys, scaling

Status and roadmap

  • Phases 0–9: core app, optimizer, menu OCR, workouts, logging, coach, content, hardening
  • CI restored with a real database, RLS-enforced tests and dependency audits
  • Phase 10: Amrita pilot, including deployment, nutritionist sign-off of the optimizer and a fact-check of the articles
  • Phase 11: post-pilot iteration

Contributing

  1. Branch from main as feat/…, fix/…, docs/… or chore/….
  2. Use Conventional Commits, one logical change per commit.
  3. Open a pull request to main. CI must be green before merge.

License

Apache License 2.0.

Interface icons: Lucide (ISC). Technology logos: techicons.dev (Devicon, MIT) and Simple Icons (CC0). Brand names and logos belong to their owners.

About

Personalised Nutrition and Workout Recommendation Engine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages