Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,20 +344,28 @@ export class McpContext implements Context {
return this.#selectedPage === page;
}

#maybeEmulateFocusedPage(page: Page, focused: boolean): void {
const maybeEmulate = (
page as Page & { emulateFocusedPage?: (focused: boolean) => Promise<void> }
).emulateFocusedPage;
if (!maybeEmulate) {
return;
}
void maybeEmulate.call(page, focused).catch(error => {
this.logger('Error toggling focused page emulation', error);
});
}

selectPage(newPage: Page): void {
const oldPage = this.#selectedPage;
if (oldPage) {
oldPage.off('dialog', this.#dialogHandler);
void oldPage.emulateFocusedPage(false).catch(error => {
this.logger('Error turning off focused page emulation', error);
});
this.#maybeEmulateFocusedPage(oldPage, false);
}
this.#selectedPage = newPage;
newPage.on('dialog', this.#dialogHandler);
this.#updateSelectedPageTimeouts();
void newPage.emulateFocusedPage(true).catch(error => {
this.logger('Error turning on focused page emulation', error);
});
this.#maybeEmulateFocusedPage(newPage, true);
}

#updateSelectedPageTimeouts() {
Expand Down
11 changes: 11 additions & 0 deletions zacino/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
POSTGRES_DB=zacino
POSTGRES_USER=zacino
POSTGRES_PASSWORD=change-me
DATABASE_URL=postgresql+asyncpg://zacino:change-me@db:5432/zacino
SECRET_KEY=replace-with-strong-secret
ACCESS_TOKEN_EXPIRE_MINUTES=60
CORS_ORIGINS=http://localhost:5173,http://localhost:4173
AUTO_CREATE_DB=true
VITE_API_URL=http://localhost:8000
BACKEND_PORT=8000
FRONTEND_PORT=5173
83 changes: 83 additions & 0 deletions zacino/.gitea/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: CI

on:
push:
branches: ["main"]
pull_request:

jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install backend dependencies
working-directory: zacino/backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-dev.txt

- name: Lint backend
working-directory: zacino/backend
run: ruff check app tests

- name: Test backend
working-directory: zacino/backend
env:
SECRET_KEY: test-secret
run: pytest

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install frontend dependencies
working-directory: zacino/frontend
run: npm ci

- name: Lint frontend
working-directory: zacino/frontend
run: npm run lint

- name: Test frontend
working-directory: zacino/frontend
run: npm test

- name: Build frontend
working-directory: zacino/frontend
run: npm run build

- name: Build Docker images
working-directory: zacino
run: docker compose build

- name: Deploy (compose up)
working-directory: zacino
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
if [ -n "$SECRET_KEY" ]; then
cp .env.example .env
sed -i "s|replace-with-strong-secret|$SECRET_KEY|" .env
docker compose up -d
else
echo "Skipping deploy: SECRET_KEY not set"
fi

- name: Notify
if: always()
env:
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
run: |
if [ -n "$WEBHOOK_URL" ]; then
curl -X POST -H 'Content-Type: application/json' \
-d '{"status":"'"$GITHUB_JOB"'","result":"'"${{ job.status }}"'"}' \
"$WEBHOOK_URL"
fi
18 changes: 18 additions & 0 deletions zacino/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/

# Node
node_modules/
/dist/

# Env
.env

# Logs
*.log

# OS
.DS_Store
21 changes: 21 additions & 0 deletions zacino/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 zacino

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
95 changes: 95 additions & 0 deletions zacino/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# zacino

**zacino** is a Meta full-stack, high-performance optimization command center for enterprise-grade teams. It provides the **Meta Core Feature**: a secure, JWT-based optimization job queue with real-time lifecycle controls and scoring.

## Tech stack

- **Backend:** FastAPI + SQLAlchemy (async) + Uvicorn
- **Frontend:** React + Vite + TypeScript
- **Database:** PostgreSQL (Docker) / SQLite (local fallback)
- **Auth:** JWT-based
- **Testing:** Pytest + Vitest
- **CI/CD:** Gitea Actions
- **Deploy:** Docker Compose with `.env` configuration

## Project structure

```
backend/ FastAPI service
frontend/ React UI
```

## Local setup

1. Copy environment variables:

```bash
cp .env.example .env
```

2. Backend (Python 3.12):

```bash
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
uvicorn app.main:app --reload
```

3. Frontend (Node 20+):

```bash
cd frontend
npm install
npm run dev
```

The frontend runs on `http://localhost:5173` and connects to the API on `http://localhost:8000`.

## Docker setup

```bash
cp .env.example .env

docker compose up --build
```

- Frontend: `http://localhost:5173`
- Backend: `http://localhost:8000`

## Tests

Backend:

```bash
cd backend
pytest
```

Frontend:

```bash
cd frontend
npm test
```

## API overview

- `POST /api/v1/auth/register` - register user
- `POST /api/v1/auth/login` - login and receive JWT
- `GET /api/v1/jobs` - list jobs
- `POST /api/v1/jobs` - create job
- `PATCH /api/v1/jobs/{id}` - update job
- `DELETE /api/v1/jobs/{id}` - delete job
- `GET /healthz` - liveness
- `GET /readyz` - readiness (DB check)

## Security notes

- Set a strong `SECRET_KEY` in `.env`.
- Configure `CORS_ORIGINS` with approved domains.

## License

MIT License. See [LICENSE](./LICENSE).
15 changes: 15 additions & 0 deletions zacino/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Empty file added zacino/backend/app/__init__.py
Empty file.
Empty file.
56 changes: 56 additions & 0 deletions zacino/backend/app/api/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import get_settings
from app.core.security import verify_password
from app.db.models import User
from app.db.session import get_session

settings = get_settings()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api_v1_prefix}/auth/login")


async def get_db() -> AsyncSession:
async for session in get_session():
yield session


async def get_current_user(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_db),
) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
subject: str | None = payload.get("sub")
except JWTError as exc:
raise credentials_exception from exc

if not subject:
raise credentials_exception

result = await session.execute(select(User).where(User.email == subject))
user = result.scalar_one_or_none()
if not user:
raise credentials_exception
if not user.is_active:
raise HTTPException(status_code=403, detail="Inactive user")
return user


async def authenticate_user(session: AsyncSession, email: str, password: str) -> User | None:
result = await session.execute(select(User).where(User.email == email))
user = result.scalar_one_or_none()
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
55 changes: 55 additions & 0 deletions zacino/backend/app/api/routes_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import authenticate_user, get_db
from app.core.config import get_settings
from app.core.security import create_access_token, get_password_hash
from app.db.models import User

router = APIRouter(prefix="/auth", tags=["auth"])
settings = get_settings()


class RegisterRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=128)


class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"


class LoginRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=8, max_length=128)


@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def register(payload: RegisterRequest, session: AsyncSession = Depends(get_db)) -> TokenResponse:
result = await session.execute(select(User).where(User.email == payload.email))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already registered")

user = User(email=payload.email, hashed_password=get_password_hash(payload.password))
session.add(user)
await session.commit()

access_token = create_access_token(subject=user.email)
return TokenResponse(access_token=access_token)


@router.post("/login", response_model=TokenResponse)
async def login(payload: LoginRequest, session: AsyncSession = Depends(get_db)) -> TokenResponse:
user = await authenticate_user(session, payload.email, payload.password)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")

access_token = create_access_token(
subject=user.email, expires_delta=timedelta(minutes=settings.access_token_expire_minutes)
)
return TokenResponse(access_token=access_token)
Loading
Loading