Files
duty-teller/site/runbook/index.html
Nikolay Tatarinov 86f6d66865
All checks were successful
CI / lint-and-test (push) Successful in 17s
chore: add changelog and documentation updates
- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format.
- Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs.
- Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings.
- Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers.
- Included new sections in the README for changelog and documentation links, improving accessibility to project information.
2026-02-20 15:32:10 +03:00

870 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Telegram bot for team duty shift calendar and group reminder">
<link rel="canonical" href="https://github.com/your-org/duty-teller/runbook/">
<link rel="prev" href="../import-format/">
<link rel="next" href="../api-reference/">
<link rel="icon" href="../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.2">
<title>Runbook - Duty Teller</title>
<link rel="stylesheet" href="../assets/stylesheets/main.484c7ddc.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<link rel="stylesheet" href="../assets/_mkdocstrings.css">
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#runbook-operational-guide" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href=".." title="Duty Teller" class="md-header__button md-logo" aria-label="Duty Teller" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Duty Teller
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Runbook
</span>
</div>
</div>
</div>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href=".." title="Duty Teller" class="md-nav__button md-logo" aria-label="Duty Teller" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
Duty Teller
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href=".." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../configuration/" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../architecture/" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../import-format/" class="md-nav__link">
<span class="md-ellipsis">
Import format
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Runbook
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Runbook
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#starting-and-stopping" class="md-nav__link">
<span class="md-ellipsis">
Starting and stopping
</span>
</a>
<nav class="md-nav" aria-label="Starting and stopping">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#local" class="md-nav__link">
<span class="md-ellipsis">
Local
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#docker" class="md-nav__link">
<span class="md-ellipsis">
Docker
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#health-check" class="md-nav__link">
<span class="md-ellipsis">
Health check
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#logs" class="md-nav__link">
<span class="md-ellipsis">
Logs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-errors-and-what-to-check" class="md-nav__link">
<span class="md-ellipsis">
Common errors and what to check
</span>
</a>
<nav class="md-nav" aria-label="Common errors and what to check">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#hash_mismatch-403-from-apiduties-or-miniapp" class="md-nav__link">
<span class="md-ellipsis">
"hash_mismatch" (403 from /api/duties or Miniapp)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#miniapp-open-in-browser-or-direct-link-access-denied" class="md-nav__link">
<span class="md-ellipsis">
Miniapp "Open in browser" or direct link — access denied
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#403-open-from-telegram-no-initdata" class="md-nav__link">
<span class="md-ellipsis">
403 "Open from Telegram" / no initData
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mini-app-url-redirect-and-broken-auth" class="md-nav__link">
<span class="md-ellipsis">
Mini App URL — redirect and broken auth
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-not-in-allowlist-403" class="md-nav__link">
<span class="md-ellipsis">
User not in allowlist (403)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#database-and-migrations" class="md-nav__link">
<span class="md-ellipsis">
Database and migrations
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../api-reference/" class="md-nav__link">
<span class="md-ellipsis">
API Reference
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#starting-and-stopping" class="md-nav__link">
<span class="md-ellipsis">
Starting and stopping
</span>
</a>
<nav class="md-nav" aria-label="Starting and stopping">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#local" class="md-nav__link">
<span class="md-ellipsis">
Local
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#docker" class="md-nav__link">
<span class="md-ellipsis">
Docker
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#health-check" class="md-nav__link">
<span class="md-ellipsis">
Health check
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#logs" class="md-nav__link">
<span class="md-ellipsis">
Logs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-errors-and-what-to-check" class="md-nav__link">
<span class="md-ellipsis">
Common errors and what to check
</span>
</a>
<nav class="md-nav" aria-label="Common errors and what to check">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#hash_mismatch-403-from-apiduties-or-miniapp" class="md-nav__link">
<span class="md-ellipsis">
"hash_mismatch" (403 from /api/duties or Miniapp)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#miniapp-open-in-browser-or-direct-link-access-denied" class="md-nav__link">
<span class="md-ellipsis">
Miniapp "Open in browser" or direct link — access denied
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#403-open-from-telegram-no-initdata" class="md-nav__link">
<span class="md-ellipsis">
403 "Open from Telegram" / no initData
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#mini-app-url-redirect-and-broken-auth" class="md-nav__link">
<span class="md-ellipsis">
Mini App URL — redirect and broken auth
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-not-in-allowlist-403" class="md-nav__link">
<span class="md-ellipsis">
User not in allowlist (403)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#database-and-migrations" class="md-nav__link">
<span class="md-ellipsis">
Database and migrations
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="runbook-operational-guide">Runbook (operational guide)</h1>
<p>This document covers running the application, checking health, logs, common errors, and database operations.</p>
<h2 id="starting-and-stopping">Starting and stopping</h2>
<h3 id="local">Local</h3>
<ul>
<li><strong>Start:</strong> From the repository root, with virtualenv activated:
<code>bash
python main.py</code>
Or after <code>pip install -e .</code>: <code>duty-teller</code></li>
<li><strong>Stop:</strong> <code>Ctrl+C</code></li>
</ul>
<h3 id="docker">Docker</h3>
<ul>
<li>
<p><strong>Dev</strong> (code mounted; no rebuild needed for code changes):
<code>bash
docker compose -f docker-compose.dev.yml up --build</code>
Stop: <code>Ctrl+C</code> or <code>docker compose -f docker-compose.dev.yml down</code>.</p>
</li>
<li>
<p><strong>Prod</strong> (built image; restarts on failure):
<code>bash
docker compose -f docker-compose.prod.yml up -d --build</code>
Stop: <code>docker compose -f docker-compose.prod.yml down</code>.</p>
</li>
</ul>
<p>On container start, <code>entrypoint.sh</code> runs Alembic migrations then starts the app as user <code>botuser</code>. Ensure <code>.env</code> (or your orchestrators env) contains <code>BOT_TOKEN</code> and any required variables; see <a href="../configuration/">configuration.md</a>.</p>
<h2 id="health-check">Health check</h2>
<ul>
<li><strong>HTTP:</strong> The FastAPI app serves the API and static webapp. A simple way to verify it is up is to open the interactive API docs: <strong><code>GET /docs</code></strong> (e.g. <code>http://localhost:8080/docs</code>). If that page loads, the server is running.</li>
<li>There is no dedicated <code>/health</code> endpoint; use <code>/docs</code> or a lightweight API call (e.g. <code>GET /api/duties?from=...&amp;to=...</code> with valid auth) as needed.</li>
</ul>
<h2 id="logs">Logs</h2>
<ul>
<li><strong>Local:</strong> Output goes to stdout/stderr; redirect or use your process managers logging (e.g. systemd, supervisord).</li>
<li><strong>Docker:</strong> Use <code>docker compose logs -f</code> (with the appropriate compose file) to follow application logs. Adjust log level via Python <code>logging</code> if needed (e.g. environment or code).</li>
</ul>
<h2 id="common-errors-and-what-to-check">Common errors and what to check</h2>
<h3 id="hash_mismatch-403-from-apiduties-or-miniapp">"hash_mismatch" (403 from <code>/api/duties</code> or Miniapp)</h3>
<ul>
<li><strong>Cause:</strong> The server that serves the Mini App (e.g. production host) uses a <strong>different</strong> <code>BOT_TOKEN</code> than the bot from which users open the Mini App (e.g. test vs production bot). Telegram signs initData with the bot token; if tokens differ, validation fails.</li>
<li><strong>Check:</strong> Ensure the same <code>BOT_TOKEN</code> is set in <code>.env</code> (or equivalent) on the machine serving <code>/api/duties</code> as the one used by the bot instance whose menu button opens the Miniapp.</li>
</ul>
<h3 id="miniapp-open-in-browser-or-direct-link-access-denied">Miniapp "Open in browser" or direct link — access denied</h3>
<ul>
<li><strong>Cause:</strong> When users open the calendar via “Open in browser” or a direct URL, Telegram may not send <code>tgWebAppData</code> (initData). The API requires initData (or <code>MINI_APP_SKIP_AUTH</code> / private IP in dev).</li>
<li><strong>Action:</strong> Users should open the calendar <strong>via the bots menu button</strong> (e.g. ⋮ → «Календарь») or a <strong>Web App inline button</strong> so Telegram sends user data.</li>
</ul>
<h3 id="403-open-from-telegram-no-initdata">403 "Open from Telegram" / no initData</h3>
<ul>
<li><strong>Cause:</strong> Request to <code>/api/duties</code> (or calendar) without valid <code>X-Telegram-Init-Data</code> header. In production, only private IP clients can be allowed without initData (see <code>_is_private_client</code> in <code>api/dependencies.py</code>); behind a reverse proxy, <code>request.client.host</code> is often the proxy (e.g. 127.0.0.1), so the “private IP” bypass may not apply to the real user.</li>
<li><strong>Check:</strong> Ensure the Mini App is opened from Telegram (menu or inline button). If behind a reverse proxy, see README “Production behind a reverse proxy” (forward real client IP or rely on initData).</li>
</ul>
<h3 id="mini-app-url-redirect-and-broken-auth">Mini App URL — redirect and broken auth</h3>
<ul>
<li><strong>Cause:</strong> If the Mini App URL is configured <strong>without</strong> a trailing slash (e.g. <code>https://your-domain.com/app</code>) and the server redirects <code>/app</code><code>/app/</code>, the browser can drop the fragment Telegram sends, breaking authorization.</li>
<li><strong>Action:</strong> Configure the bots menu button / Web App URL <strong>with a trailing slash</strong>, e.g. <code>https://your-domain.com/app/</code>. See README “Mini App URL”.</li>
</ul>
<h3 id="user-not-in-allowlist-403">User not in allowlist (403)</h3>
<ul>
<li><strong>Cause:</strong> Telegram users username is not in <code>ALLOWED_USERNAMES</code> or <code>ADMIN_USERNAMES</code>, and (if using phone) their phone (set via <code>/set_phone</code>) is not in <code>ALLOWED_PHONES</code> or <code>ADMIN_PHONES</code>.</li>
<li><strong>Check:</strong> <a href="../configuration/">configuration.md</a> for <code>ALLOWED_USERNAMES</code>, <code>ADMIN_USERNAMES</code>, <code>ALLOWED_PHONES</code>, <code>ADMIN_PHONES</code>. Add the user or ask them to set phone and add it to the allowlist.</li>
</ul>
<h2 id="database-and-migrations">Database and migrations</h2>
<ul>
<li><strong>Default DB path (SQLite):</strong> <code>data/duty_teller.db</code> (relative to working directory when using default <code>DATABASE_URL=sqlite:///data/duty_teller.db</code>). In Docker, the entrypoint creates <code>/app/data</code> and runs migrations there.</li>
<li><strong>Migrations (Alembic):</strong> From the repository root:
<code>bash
alembic -c pyproject.toml upgrade head</code>
Config: <code>pyproject.toml</code><code>[tool.alembic]</code>; script location <code>alembic/</code>; metadata and URL from <code>duty_teller.config</code> and <code>duty_teller.db.models.Base</code>.</li>
<li><strong>Rollback:</strong> Use with care; test in a copy of the DB first. Example to go back one revision:
<code>bash
alembic -c pyproject.toml downgrade -1</code>
Always backup the database before downgrading.</li>
</ul>
<p>For full list of env vars (including <code>DATABASE_URL</code>), see <a href="../configuration/">configuration.md</a>. For reverse proxy and Mini App URL details, see the main <a href="../README.md">README</a>.</p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": [], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>