Skip to content
GoatWhistlePublic

About

Reminders reach Telegram or email before a registration deadline closes, and a status shows only what a person marked, never what the system guessed. Hackathon participation tracker for participants and the administrators who assign them

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

366 Commits

Folders and files

Repository files navigation

Timeline: every hackathon you are in, on one timeline

For people who take part in many hackathons and the administrators who line them up: assignments, stages, deadlines and reminders in one place, so a registration window never closes while you were looking the other way.

Run it locally API contract

registration: self-reported, never verified reminders: Telegram and email
delivery: at least once interface: RU and EN license: MIT

frontend: React 19 and TypeScript API: Go 1.25
storage: PostgreSQL 16 contract: OpenAPI runs in: Docker Compose

The rule What it does How it works Running it How it is built Limitations

Event card as a participant sees it
The participant's event card on a local stack. The badge reads You marked it, and the line under the form repeats why: the system does not check the external site.

The rule

Timeline never checks the external registration site. A registration status is the participant's own report, or a state an administrator set, never a confirmed fact.

The interface says so in as many words: "you marked it", not "confirmed". The status carries its source (participant, administrator or system) in its own column, a bulk change by an administrator is limited to TODO and NOT_REQUIRED, and the move from NOT_REQUIRED to DONE is refused with 409 invalid_state_transition instead of being applied quietly.

What it does

An administrator creates an event, assigns participants and configures reminders. A participant sees what they are assigned to, and marks for themselves that they have registered. Reminders go out over Telegram or email.

Participants also form standing teams and enter an event with a lineup drawn from one; each keeps a private layer of tasks, notes, submissions, an outcome and a retrospective against every event they took part in.

Catalog

Event catalog

Every event, as tiles with the fields you pick or as a sortable table with the columns you pick. Filters, CSV export, and a tick on the ones you are in.

Schedule

Personal schedule

Your assignments over a week, month, quarter, year, or the whole timeline at once. Each event keeps one colour so a crowded month still reads.

Stages

Stage editor

Applications, selection, build, checkpoint, submission, demo, results. A stage may sit outside the event period: that is allowed, and only warns.

Registration statuses

Registration statuses

Who reported, who did not, and who set it: the source column separates the participant from an administrator from the system.

How it works

  1. Create. An administrator adds an event: period, format, tracks and prizes, team size, and the stages it runs through.
  2. Assign. Participants are added in a batch, each with a team role. Every item is its own transaction, so a partial failure still reports per row.
  3. Set the deadline. A registration carries the organiser's link and a deadline, which may never fall after the event ends.
  4. Schedule reminders. Up to ten rules per registration, each an offset before the deadline. The scheduler turns them into notifications, one per business situation: a versioned logical key drops the repeat.
  5. Report. The participant opens the organiser's form, registers there, and marks it here.
  6. Deliver. The worker takes notifications under a lease and sends them to Telegram or email.
%%{init: {"theme":"base","themeVariables":{"primaryColor":"#ffffff","primaryTextColor":"#1e1a14","primaryBorderColor":"#ac4600","secondaryColor":"#efeae5","tertiaryColor":"#faf7f3","lineColor":"#665f56","textColor":"#1e1a14","fontFamily":"Geist"}}}%%
flowchart LR
    SPA["React SPA"] --> Edge["nginx, one origin"]
    Edge --> API["Go API<br/>controllers"]
    API --> SVC["services<br/>domain rules"]
    SVC --> REPO["adapters<br/>SQL over pgx"]
    REPO --> DB[("PostgreSQL")]
    SCHED["scheduler"] --> DB
    DB --> WORKER["worker<br/>lease and retry"]
    WORKER --> TG["Telegram"]
    WORKER --> MAIL["email"]
Loading

The API and the SPA are served from one origin, so the session cookie needs no CORS and no SameSite=None. The scheduler and the worker are one separate process beside the API, reading the same database.

Running it

Everything runs in containers: Go, Node and PostgreSQL are not needed on the machine, Docker is enough.

make dev

The application comes up on http://localhost:5173, the API on http://localhost:8080, and Swagger UI on /docs/. Use make docker-up instead for the production build behind nginx, and make help for everything else.

No account is created automatically. Create the first administrator with the backend CLI, then sign in with it:

docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml \
  run --rm --no-deps backend go run ./cmd/timeline admin create \
  --email you@example.com --password 'a-long-password' --name 'Your Name' --nickname you
Configuration, Telegram and checks

Configuration comes only from environment variables; .env.example is the full list, and make docker-up creates .env from it.

Telegram setup and connection diagnostics: Telegram deployment guide.

Before a change counts as done, make verify has to pass: the contract, types, linters and tests for both parts. make test-contract runs the contract tests against the real API in a container, and make e2e signs in through a real browser against the same stack.

How it is built

A modular monolith in Go behind a React SPA, with a separate worker process for delivery.

backend/       Go: cmd, internal/{config,models,services,adapters,controllers,cli}, migrations
frontend/      React + TypeScript + Vite, Tailwind + shadcn/ui, TanStack Query, react-router
deploy/        Dockerfiles, nginx.conf, docker-compose.yml (+ .dev.yml)
docs/          The generated OpenAPI contract, ratchet baselines and these illustrations
scripts/       Ratchet checks and the README renderer, called from the Makefile

Inside the backend the layers are strict: controllers know about HTTP, services hold domain rules and transaction boundaries, adapters/repositories is SQL over pgx, and models is domain types and errors. A controller never reaches into a repository directly. The frontend is organised by domain: src/entities for the things themselves, src/features for what is done with them, shared code in src/shared, tokens in src/styles, and the shell and routes in src/app.

The API contract lives in docs/swagger.yaml, generated from annotations beside the controllers and never edited by hand. It is written before the implementation, so both halves proceed from one description.

Several rules are enforced by machine rather than by review: a 250-line limit per file and a file-count limit per package, both as ratchets over a recorded baseline that only moves down; no import cycles, counting test imports too; no Cyrillic outside the localisation dictionaries; and images only under screenshots/, frontend/public, frontend/src/assets or docs/. The banner, buttons and badges above are rendered from the application's own tokens, font and mark by make readme.

Limitations

  • A registration status is not a fact. Nothing queries the organiser's site, so a participant who marks DONE without registering stays DONE until someone corrects it.
  • Delivery is at least once, not exactly once. The worker claims notifications with FOR UPDATE SKIP LOCKED under a lease; a worker that dies after sending and before recording the send leaves the job to the next one, and the reminder arrives twice.
  • Three defaults turn protection off so that http://localhost needs no setup, and all three must change on deployment. COOKIE_SECURE=false lets session cookies travel over plain HTTP; an empty METRICS_TOKEN leaves /metrics open to anyone who can reach the port; an empty SMTP_HOST writes mail to the log instead of sending it. The backend warns about each one at start.


Team GoatWhistle, 2026. Licensed under MIT. Geist and Geist Mono are used under the SIL Open Font License.

About

Reminders reach Telegram or email before a registration deadline closes, and a status shows only what a person marked, never what the system guessed. Hackathon participation tracker for participants and the administrators who assign them

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages