A substitute teacher management app for K-12 school operations. Sub-Bot helps administrators quickly find and assign available substitute teachers when absences are reported.
- Teacher Profiles - Full schedule grids (8 periods x 5 days), nickname, teacher type, cannot-subject restrictions, availability preferences, max subs per week
- Absence Reporting - Report same-day or future absences with period and subject details
- Smart Sub Matching - Ranked algorithm that filters by availability, subject match, workload balance, and teacher preferences
- AI-Powered CSV Import - Import teacher schedules from per-teacher CSV files using AI (Claude, GPT, Gemini, or OpenRouter), including per-user OpenAI API key/OAuth option, with regex fallback
- Telegram General AI Agent - Understand absence reports, answer availability queries, and handle safe general Q&A with always-confirm-before-write behavior
- In-App Subrina Chat - Dedicated
/chatexperience with persisted history, hybrid streaming (Q&A tokens), and the same operational AI capabilities as Telegram - Semester Calendar - Configured with real school dates, period times, and holidays
- Dashboard - At-a-glance view of today's absences, coverage stats, and quick actions
- Next.js 16 (App Router, TypeScript, Tailwind CSS v4)
- JSON file storage with file locking (no database required)
- AI parsing via Anthropic, OpenAI, Google Gemini, or OpenRouter SDKs
- Zod for validation
- PapaParse for CSV processing
- date-fns for date utilities
- Node.js 18+
- npm
git clone https://github.com/leepeffer/sub-bot.git
cd sub-bot
npm installCopy the environment template and add your AI provider API key:
cp .env.local.example .env.localEdit .env.local:
# Pick a provider: anthropic | openai | gemini | openrouter
AI_PROVIDER=anthropic
# Add the API key for your chosen provider
ANTHROPIC_API_KEY=sk-ant-...Configure Firebase for both browser auth and server access:
# Firebase Web SDK (client)
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
# Firebase Admin SDK (server)
FIREBASE_PROJECT_ID=
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=Configure sign-in access policy:
# Demo-safe default: deny by default unless explicitly allowlisted.
AUTH_ALLOW_ALL=false
AUTH_ALLOWED_DOMAINS=example.org
AUTH_ALLOWED_EMAILS=admin@example.orgOptional OpenAI OAuth setup (for per-user connect flow in Settings):
OPENAI_OAUTH_CLIENT_ID=
OPENAI_OAUTH_CLIENT_SECRET=
OPENAI_OAUTH_REDIRECT_URI=http://localhost:3000/api/ai/openai/oauth/callback
OPENAI_OAUTH_SCOPES=openid profile email offline_accessOptional Telegram intent AI setup:
# Server defaults (can be overridden in Settings -> Telegram Intent AI)
TELEGRAM_INTENT_AI_ENABLED=false
# Enables multi-intent Telegram routing (availability + Q&A)
TELEGRAM_GENERAL_AGENT_ENABLED=false
# Provider for Telegram message parsing: openrouter | openai | anthropic
TELEGRAM_INTENT_AI_PROVIDER=openrouter
# Optional model override
# TELEGRAM_INTENT_AI_MODEL=x-ai/grok-4.1-fast
# Optional reasoning/thinking level: off | low | medium | high
TELEGRAM_INTENT_AI_REASONING_LEVEL=off
TELEGRAM_INTENT_AI_MIN_CONFIDENCE=0.65
TELEGRAM_INTENT_AI_TIMEOUT_MS=12000
TELEGRAM_INTENT_AI_MAX_OCCURRENCES=20
# Required for strict CLI wrapper mode in Telegram General Agent
TELEGRAM_AGENT_API_TOKEN=sbt_live_...Optional in-app Subrina chat defaults:
# Independent feature flag for /chat (overridable in Settings UI)
SUBRINA_WEB_CHAT_ENABLED=false
# Retention window for persisted chat history
SUBRINA_CHAT_RETENTION_DAYS=30
# Optional dedicated CLI wrapper auth token for in-app chat
# Falls back to TELEGRAM_AGENT_API_TOKEN if omitted
SUBRINA_AGENT_API_TOKEN=sbt_live_...For temporary public demos, use these environment flags:
DEMO_READ_ONLY=true
DEMO_DISABLE_AI=true
DEMO_DISABLE_TELEGRAM=true
DEMO_DISABLE_TOKEN_MANAGEMENT=trueRecommended deployment posture:
- Use a separate Firebase demo project (not production).
- Keep allowlist auth enabled (
AUTH_ALLOW_ALL=false+ explicit allowlist). - Use read-only mode so authenticated users cannot mutate data.
Telegram intent provider/key resolution order:
- Saved Settings UI configuration (
/settings-> Telegram Intent AI) + provider key - Env defaults (
TELEGRAM_INTENT_AI_PROVIDER, thenAI_PROVIDER) + provider key - Regex parser fallback (no LLM)
Subrina in-app chat settings resolution order:
- Saved Settings UI configuration (
/settings-> Subrina In-App Chat) - Env defaults (
SUBRINA_WEB_CHAT_ENABLED,SUBRINA_CHAT_RETENTION_DAYS) - Chat disabled (if not enabled by either)
Note: AI is optional. If no provider is configured, CSV import falls back to a regex-based parser.
This project currently deploys with Firebase Hosting + frameworksBackend (SSR Cloud Function), not Firebase App Hosting.
For Telegram runtime secrets, use Google Cloud Secret Manager with these exact secret ids:
telegram-bot-tokentelegram-webhook-secret
The app reads those secrets directly at runtime via Secret Manager API.
Grant your SSR runtime service account access to both secrets (roles/secretmanager.secretAccessor).
Without this IAM binding, Telegram setup/webhook calls will fail with permission errors.
Required production environment config (set in GitHub for deploy):
- Repository variable:
APP_BASE_URL(for examplehttps://sub-bot-413a6.web.app) - Repository variable:
FIREBASE_PROJECT_ID(demo project id for temporary public demo) - Repository variable:
AUTH_ALLOW_ALL(defaultfalse) - Repository variable:
AUTH_ALLOWED_DOMAINS(comma-separated) - Repository variable:
AUTH_ALLOWED_EMAILS(comma-separated) - Repository variable:
DEMO_READ_ONLY(recommendedtrue) - Repository variable:
DEMO_DISABLE_AI(recommendedtrue) - Repository variable:
DEMO_DISABLE_TELEGRAM(recommendedtrue) - Repository variable:
DEMO_DISABLE_TOKEN_MANAGEMENT(recommendedtrue) - Repository secret:
TELEGRAM_WEBHOOK_ADMIN_EMAILS(comma-separated admin emails) - Optional repository variable:
SCHOOL_TIMEZONE(defaultAmerica/Sao_Paulo) - For Telegram AI parser in production, also set at least one provider key secret:
OPENROUTER_API_KEY(recommended), orOPENAI_API_KEY, orANTHROPIC_API_KEY
- For strict Telegram CLI write/read tool mode, set secret:
TELEGRAM_AGENT_API_TOKEN
- For strict in-app Subrina chat CLI write/read tool mode (optional dedicated token):
SUBRINA_AGENT_API_TOKEN(falls back toTELEGRAM_AGENT_API_TOKEN)
Then deploy:
npx firebase-tools@latest deploy --force --project <your-firebase-project-id>Note: apphosting.yaml is present in this repo, but it does not control the current Hosting+SSR runtime.
npm run devOpen http://localhost:3000.
sub-bot/
├── data/ # JSON data files (teachers, absences, semester config)
├── src/
│ ├── app/
│ │ ├── page.tsx # Dashboard
│ │ ├── teachers/ # Teacher list, profiles, edit, import
│ │ │ ├── import-schedule/ # AI-powered per-teacher CSV import
│ │ │ └── import/ # Bulk CSV import (tabular format)
│ │ ├── absences/ # Absence reporting and sub assignment
│ │ ├── calendar/ # Semester calendar view
│ │ ├── chat/ # In-app Subrina chat interface
│ │ ├── settings/ # Semester config, period times, holidays
│ │ └── api/ # REST API routes
│ │ ├── teachers/ # CRUD + import endpoints
│ │ ├── absences/ # CRUD + assignment
│ │ ├── matching/ # Sub matching algorithm
│ │ ├── semester/ # Semester config
│ │ └── stats/ # Dashboard statistics
│ ├── components/ # Reusable React components
│ ├── lib/
│ │ ├── ai/ # AI provider abstraction (4 providers)
│ │ ├── matching.ts # Sub matching & ranking algorithm
│ │ ├── schedule-csv-parser.ts # Regex fallback CSV parser
│ │ ├── data.ts # JSON file persistence layer
│ │ ├── dates.ts # School date utilities
│ │ └── validation.ts # Zod schemas
│ └── types/index.ts # TypeScript interfaces
└── .env.local # API keys (gitignored)
When an absence is reported, Sub-Bot finds and ranks available substitutes:
Hard filters (disqualify a teacher):
- Not the absent teacher
- Not also absent that day
- Not teaching during the requested period(s)
- Not already assigned as sub elsewhere
- Must not be restricted from the needed subject
- Must not exceed weekly sub limit
Soft scoring (rank remaining candidates from a base of 100):
| Factor | Impact |
|---|---|
| Prefers this slot | +15/period |
| Prefers NOT this slot | -30/period |
| Weekly sub count | -10/sub |
| Semester sub count | -1/sub |
| Is a TA | -5 |
| Already subbing adjacent period | +20 (continuity) |
Teacher schedule CSVs are expected as one file per teacher in a grid format. Sub-Bot supports 4 AI providers to intelligently parse these, with automatic fallback to regex:
AI configured? ──YES──> Call provider ──> Validate ──> Confidence >= 50%? ──YES──> Use AI result
│ │
NO NO
│ │
└──────────────────> Regex fallback <───────────────────┘
Configure in .env.local:
anthropic- Claude (default: claude-sonnet-4-20250514)openai- GPT (default: gpt-4o)gemini- Gemini (default: gemini-2.0-flash)openrouter- Any model via OpenRouter (default: anthropic/claude-sonnet-4)
Credential precedence for schedule import:
- Signed-in user's OpenAI credential from Settings (API key or OAuth)
- Server-level provider from
AI_PROVIDER+ provider API key - Regex fallback parser
Telegram now uses an AI-first multi-intent router with strict JSON schema validation and normalization:
Telegram message -> AI parser (optional) -> schema validation -> normalize dates/periods
-> intent routing (report_absence | availability_query | general_qna | unknown)
-> confidence + required fields check -> accept
-> else regex fallback for operational intents -> accept/clarify
Supported patterns:
- Single date:
"Maria is out periods 1,2,3 tomorrow" - Time range:
"Maria absent 09:00-11:10 2026-03-02" - Date range:
"Maria periods 2-4 from 2026-03-02 to 2026-03-06" - Recurring:
"Maria absent all day every Monday until 2026-04-30" - Availability:
"Who's available to cover sixth period today?"
Behavior:
report_absence: always confirms before write, then upserts absence and suggests substitutes.availability_query: returns ranked free teachers for requested date/period.general_qna: answers in chat mode with no writes/tool side effects.- Single-date absence keeps interactive substitute assignment buttons in Telegram.
- Multi-date absence creates/updates each date and returns substitute suggestions per date in a digest.
- Existing teacher/date absences are upserted (periods merged) instead of duplicated.
- In General Agent mode, operational actions prefer the strict internal CLI gateway (
node dist-cli/cli/index.js ... --json). - Default OpenRouter model for Telegram intent parsing is
x-ai/grok-4.1-fastwhen no model override is set.
- Route:
/chat(authenticated users only) - Backed by one active thread per user, stored in Firestore with message retention
- Operational actions always require explicit confirmation before write operations
- Q&A responses stream tokens in real time; operational actions return final structured responses
- Uses the same intent parsing/provider configuration as Telegram AI settings
Default demo semester configuration:
- Dates: January 22 - June 11, 2026
- Periods: 8 per day (8:13 AM - 3:25 PM)
- Holidays: 11 days (Carnaval, Good Friday, Tiradentes, Labor Day, Corpus Christi, etc.)
No license file is included yet. Add one before public redistribution if needed.