Stempelbot 🤖⏱️
A small Streamlit-based helper that automates and visualizes stamping in/out on the
coeo employee portal (mitarbeiterinfo.coeo.de). It provides a live dashboard of your
current working day, lets you stamp manually with one click, and can auto-stamp at a
scheduled time in the background.
Features
- Live work-day metrics – start time, target end time (8h), net/gross work, taken breaks and legally required break deductions, updated every few seconds.
- Manual stamping – single button that logs in to the coeo portal and sends a stamp request, storing the timestamp locally.
- SmartTime Plus verification – after every stamp the app opens the internal SmartTime Plus dashboard (headless Chromium) and confirms the "Letzte Buchung" actually moved. Failed transmissions are surfaced instead of silently missed.
- Auto-Stamp planner – schedule one or more times for today; a background thread fires the stamp within a 2-minute window of the target time and cleans up missed entries.
- Daily timeline – two-column "Kommen / Gehen" view of every stamp of the day.
- Local persistence – stamps and scheduled auto-stamps are stored as JSON files
in the project directory (
timestamp_history.json,autostamps.json). - Weekly Jira review – a separate Streamlit page shows one likely task per day, its Jira activity, and editable module/project/use-case/phase recommendations.
- Excel export – fills the provided workbook template with every day's complete net stamped time after legal break deductions. Optional Azure OpenAI classification is used when configured; a local deterministic fallback remains available.
Project layout
stempelbot/
Stempelbot.py # Streamlit UI
cli.py # `stempelbot` entry point → runs `streamlit run Stempelbot.py`
client.py # CoeoClient: login + stamp HTTP calls
smarttime_client.py # Stamp verification and guarded resends
smarttime_browser.py # Isolated browser jobs with timeout/recovery
smarttime_ui.py # SmartTime 9 login, dashboard and monthly DOM reader
autostamp.py # AutoStampService: background scheduler
stamp_history.py # Local JSON stamp history
time_calculator.py # Work/break time calculations
jira_client.py # Jira Cloud activity retrieval and normalization
weekly_report.py # Weekly aggregation and task recommendations
excel_export.py # Template-preserving XLSX export
pages/ # Weekly Jira & Excel Streamlit page
settings.py # Pydantic settings loaded from .env
scripts/
smarttime_cli.py # Read-only status / complete month JSON export
smarttime_probe.py # Interactive browser exploration and snapshots
Installation
Requires Python 3.12+ and Poetry.
poetry install
poetry run playwright install chromium
The playwright install step downloads a ~150 MB Chromium build that the
SmartTime verifier uses in headless mode. It only needs to be run once.
.env setup
settings.py uses pydantic-settings and expects a .env file in the working
directory from which you start the app (typically the project root). The core stamping
variables shown first are required; Jira, Azure OpenAI, and workbook overrides are
optional:
# Credentials for https://mitarbeiterinfo.coeo.de
USERNAME=your.user
PASSWORD=your-password
# Password for the internal SmartTime Plus web app (username is USERNAME).
# Used to verify that stamps actually reached the backend.
SMART_TIME_PASSWORD=your-smarttime-password
# Optional overrides:
# SMART_TIME_URL=https://zeiterfassung/
# SMART_TIME_VERIFY_INITIAL_DELAY=1 # seconds to wait before the first check
# SMART_TIME_VERIFY_TOLERANCE_SEC=90 # allowed drift between stamp & backing time
# Paths (relative or absolute) to the JSON files used for local persistence.
# The files will be created on first write.
AUTOSTAMP_FILE=autostamps.json
STAMPHISTORY_FILE=timestamp_history.json
# Python logging level, e.g. DEBUG / INFO / WARNING / ERROR
LOG_LEVEL=INFO
# Optional: weekly Jira reporting (Jira Cloud API token authentication)
# Use the tenant origin only; browser paths are normalized automatically.
JIRA_URL=https://your-company.atlassian.net
JIRA_EMAIL=you@example.com
JIRA_API_TOKEN=your-jira-api-token
# Optional for scoped API tokens; normally discovered automatically after a 401.
# JIRA_CLOUD_ID=00000000-0000-0000-0000-000000000000
# Optional custom JQL. {start} and {end} are replaced with ISO dates; end is exclusive.
# JIRA_JQL=project in (ABC, XYZ) AND updated >= "{start}" AND updated < "{end}"
# JIRA_TIMEOUT_SEC=30
# JIRA_VERIFY_TLS=true
# Optional: improve the daily classification with an Azure OpenAI deployment
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY=your-azure-openai-key
AZURE_OPENAI_DEPLOYMENT=gpt-5.6-luna
# AZURE_OPENAI_API_VERSION=2025-04-01-preview
# Optional workbook defaults
TIMETRACKING_NAME=Firstname Lastname
# TIMETRACKING_TEMPLATE_FILE=C:\path\to\template.xlsx
# TIMETRACKING_EXPORT_LOCATION=C:\path\to\weekly\exports
⚠️ Credentials are sent to the coeo portal from your local machine. Keep the
.envfile out of version control.
Creating a Jira API token
This integration uses an Atlassian API token, not your Jira password. The token is used with your Atlassian account email through HTTP Basic authentication and only reads Jira data.
-
Sign in to Atlassian account security.
-
Select Create API token.
-
Choose either:
- API token (unscoped/classic) for the simplest setup; or
- API token with scopes to restrict the token to Jira read access.
-
Give it a descriptive name such as
stempelbot, select an expiration date, and—if creating a scoped token—select Jira as the app and grant the read scopes listed below. -
Create and immediately copy the token. Atlassian only displays it once.
-
Add your Jira tenant origin, Atlassian account email, and token to
.env:JIRA_URL=https://your-company.atlassian.net JIRA_EMAIL=you@example.com JIRA_API_TOKEN=paste-the-token-here
For a scoped token, grant read access for:
- your current user/profile;
- issues and issue details;
- issue changelogs;
- comments;
- worklogs;
- projects, users, fields, statuses, and issue types used by search/JQL.
In Atlassian's scope selector these are typically represented by scopes such as
read:me, read:issue:jira, read:issue-details:jira,
read:issue.changelog:jira, read:comment:jira, read:issue-worklog:jira,
read:project:jira, read:user:jira, read:field:jira, read:status:jira,
read:issue-type:jira, and read:jql:jira. Atlassian may group or rename individual
granular scopes over time; no write or administration scope is required. If offered,
the classic Jira scope read:jira-work covers the required Jira work data.
The Jira user must also have normal Jira permissions to browse the relevant projects and view their comments and worklogs. An API token cannot grant access that the user does not already have.
For scoped tokens, the app automatically discovers the tenant Cloud ID and switches to
Atlassian's scoped-token gateway. JIRA_CLOUD_ID normally does not need to be set.
If authentication fails:
- verify that
JIRA_EMAILis the email belonging to the account that created the token; - set
JIRA_URLto the tenant origin, not a board or project URL; - check that the token has not expired or been revoked;
- confirm that the scoped token includes the Jira read scopes above; and
- restart the Streamlit process after changing
.env.
Usage
Start the Streamlit app via the Poetry script:
poetry run stempelbot
This is equivalent to streamlit run stempelbot/Stempelbot.py. Any extra CLI arguments
are forwarded to Streamlit, e.g.:
poetry run stempelbot --server.port 8502
Then open the URL Streamlit prints (default: http://localhost:8501).
Manual stamp
Click "Jetzt Stempeln" – the client logs in and posts the stamp request; on
success the current time is appended to timestamp_history.json.
Auto-Stamp
- Pick a time in the Auto-Stempel Planer section.
- Click "Hinzufügen" – the entry is written to
autostamps.json. - A background thread (
AutoStampService) checks every 30 seconds and fires the stamp when the current time is within a 2-minute window of the target. Missed entries (older than 2 minutes) are cleaned up automatically.
Because the scheduler runs inside the Streamlit process, the app must be running for auto-stamps to fire.
Daily view
The "Heutiger Verlauf" section lists all of today's stamps split into Kommen (even index) and Gehen (odd index) columns.
Weekly Jira & Excel export
Open Weekly Jira & Excel in Streamlit's page navigation, choose any date in the desired ISO week, and click Load Jira activity & recommendations. The page:
- loads issues created, assigned, reported, or worklogged by the configured Jira user;
- retains authored comments, changelog entries, worklogs, and relevant assigned updates;
- calculates net time from complete local stamp pairs for each day;
- recommends one workbook classification and one short task summary per day;
- allows every classification and summary to be edited before exporting the workbook.
When TIMETRACKING_EXPORT_LOCATION is configured, the workbook is saved directly to
that directory. If it is unset or empty, the page provides a browser download button
instead.
The full net duration for a day is placed on exactly one row, so no stamped minute is split or omitted. Incomplete historical stamp pairs are not guessed: the open pair is ignored and visibly flagged for review. Jira and Azure credentials are optional for normal stamping; without Jira, the weekly page still loads local time with editable fallback rows. Azure receives only the selected week's normalized Jira metadata and net minutes, and is skipped entirely when its settings are absent.
The output is a new in-memory .xlsx; the source template is never overwritten.
Dropdowns are restored as portable Excel validations after export, and formulas/styles,
merged cells, sheet names, and print layout are retained. Formula recalculation is
requested when the generated workbook is opened in Excel.
Both regular and scoped Atlassian API tokens are supported. Regular tokens use the
tenant REST URL. If that URL returns HTTP 401, the client discovers the tenant Cloud ID
and retries through https://api.atlassian.com/ex/jira/{cloudId} as required for scoped
tokens. Set JIRA_CLOUD_ID only if automatic discovery is unavailable.
How stamping works
CoeoClient in client.py performs two HTTP POSTs against
https://mitarbeiterinfo.coeo.de:
POST /ppeiiefdna_loginwithhttpd_username/httpd_password.POST /index.phpwithc=zeiterfassung&homeoffice=1to toggle the stamp.
The session cookie from step 1 is reused for step 2. HTTP 200 is treated as success.
Because a 200 from the coeo portal does not guarantee the stamp reached the
downstream time-tracking system, SmartTimeClient (see smarttime_client.py)
then uses a headless Chromium (Playwright) to log in to SmartTime Plus
(https://zeiterfassung/), navigate to the dashboard, and read the
"Letzte Buchung" value. If it doesn't match the sent stamp within
SMART_TIME_VERIFY_TOLERANCE_SEC after SMART_TIME_VERIFY_INITIAL_DELAY seconds, the
UI flags the stamp as unverified.
SmartTime 9.0 uses Vaadin Flow. The reader logs in using username / password,
opens the root dashboard through its sidebar entry, and reads the personal
Letzte Buchung card. Presence comes from that card's building icon, matched to
the dashboard's status-color legend. The old GWT hash route and global status-text
matching are no longer used.
Each read runs in a fresh headless browser subprocess. A stuck job is terminated with its browser children and retried, up to three read attempts, with two seconds between attempts. The hard limit per attempt is 75 seconds for dashboard reads and 180 seconds for a monthly export. A permanent outage returns an error; it cannot leave the shared checker stuck on an old worker. Recognized login rejections stop immediately. Reads no longer use the old persistent browser profile.
Stamping allows five total stamp attempts, with a five-second wait before each resend. A fresh SmartTime check after the wait can catch a delayed booking and cancel the resend. Only a complete, same-day snapshot with an older booking allows a resend; unreadable, missing, future or ambiguous results stop the workflow. Each resend is verified against its own send time. The manual-stamp spinner shows attempts and waits. Browser recovery attempts only read data; they never send stamps.
Standalone SmartTime CLI
Run from the repository root with the same .env used by the app:
# Check login and retrieve the current last booking and presence.
poetry run python scripts/smarttime_cli.py
# Export every day of a selected month to a new JSON file.
poetry run python scripts/smarttime_cli.py --month 2026-09 --output .local/smarttime/2026-09.json
# Also capture screenshots and sanitized HTML, including Vaadin shadow DOM.
poetry run python scripts/smarttime_cli.py --month 2026-09 --artifacts .local/smarttime/inspection
Alternatively use .venv\Scripts\python.exe in place of poetry run python.
The CLI is read-only: it never calls the coeo stamping endpoint or modifies local
stamp history. Existing output files are refused. Diagnostic files under .local/
are git-ignored; they contain personal time data, so keep them local.
The monthly reader navigates to Monatsübersicht (/MonthlyOV), selects the
month/year, and scrolls the virtualized table to collect all calendar dates. It
rejects missing dates, duplicate dates, changed columns or inconsistent repeated
rows instead of exporting a partial month. JSON contains the ordered booking times,
Soll/Ist, daily/cumulative balances, breaks, absence labels and original cell text.
Unpaired bookings are marked incomplete_pair; no missing departure is invented.
SmartTime's displayed totals are preserved as-is, including provisional current-day
and future-day values. This export is not yet wired into weekly reports.
Implementation notes and the observed SmartTime 9 selectors are in
docs/smarttime-9.md.
SmartTime Abgleich panel
Below the daily timeline there's a "SmartTime Abgleich" panel with a "Jetzt prüfen" button that compares the last local stamp with the value reported by SmartTime Plus and highlights any mismatch.
Notes
- Time calculations (net work, break deductions, target end time) live in
time_calculator.py. - Streamlit caches the
CoeoClient(lru_cache) and theAutoStampService(st.cache_resource) so login state and the scheduler thread survive reruns. - This project targets the internal coeo portal and is not affiliated with coeo.