No description
  • Kotlin 70.4%
  • Go 28.7%
  • Shell 0.5%
  • Dockerfile 0.4%
Find a file
Jens Kulka 6219ed9e1a
Some checks failed
Build and Deploy Backend / build-and-push (push) Has been cancelled
Build and Deploy Backend / deploy (push) Has been cancelled
Build and Deploy Backend / test (push) Has been cancelled
initial commit
2026-10-03 15:28:17 +02:00
.forgejo/workflows initial commit 2026-10-03 15:28:17 +02:00
app initial commit 2026-10-03 15:28:17 +02:00
backend initial commit 2026-10-03 15:28:17 +02:00
gradle initial commit 2026-10-03 15:28:17 +02:00
.gitignore initial commit 2026-10-03 15:28:17 +02:00
build.gradle.kts initial commit 2026-10-03 15:28:17 +02:00
deploy.sh initial commit 2026-10-03 15:28:17 +02:00
docker-compose.yml initial commit 2026-10-03 15:28:17 +02:00
gradle.properties initial commit 2026-10-03 15:28:17 +02:00
gradlew initial commit 2026-10-03 15:28:17 +02:00
README.md initial commit 2026-10-03 15:28:17 +02:00
settings.gradle.kts initial commit 2026-10-03 15:28:17 +02:00

Moodtracker

A small mood tracker for two (or a handful of) friends. Each person logs their mood on a 5-point scale (−2…+2) with an optional note, and both of you can see when the other's mood moves between negative / neutral / positive. You can also opt in to a Gotify push when that happens.

  • Backend: Go + Gin + SQLite (backend/), shipped as a Docker image
  • App: Android, Kotlin + Jetpack Compose (app/), offline-first with Room + WorkManager
Score Category
+2 😄 Great positive
+1 🙂 Good positive
0 😐 Okay neutral
−1 🙁 Low negative
−2 😣 Awful negative

Running the backend locally

cp backend/config.example.json backend/config.json   # set an invite_code
docker compose up -d --build                          # API on http://localhost:8081
docker compose --profile gotify up -d                 # optional: local Gotify on http://localhost:8082 (admin/admin)

Or without Docker: cd backend && go run . (listens on :8080, DB in backend/data/). Tests: cd backend && go test ./...

Configuration

config.json has one key:

{ "invite_code": "some-long-random-string" }

You need the invite code to register, so only people you give it to can join. Everyone registered on the server sees everyone else's moods. Environment variables PORT, DB_PATH and CONFIG_PATH override the defaults.

Security

  • Rate limit: after 5 failed logins or wrong invite codes from one IP within 15 minutes, that IP gets 429 until the window passes. A successful login clears the counter. The limit is kept in memory, so a restart resets it.
  • Client IPs behind a proxy: X-Forwarded-For is only trusted from TRUSTED_PROXIES (default 127.0.0.1,::1,172.16.0.0/12; the latter is Docker's bridge, where a proxy on the host shows up). Nobody else can fake their IP to dodge the limit. If your proxy runs elsewhere, set TRUSTED_PROXIES to its address.

Deploying to your server (Forgejo CI)

.forgejo/workflows/deploy.yml works the same way as in PushBattles. On every push to main that touches backend/, it runs the Go tests, builds and pushes the image to your Forgejo registry, then SSHes to the server and runs it from ~/moodtracker.

Repository secrets:

Name Description
FORGEJO_URL Registry host, e.g. git.example.com
FORGEJO_TOKEN Access token with package write access
DEPLOY_HOST / DEPLOY_PORT / DEPLOY_USER SSH target
DEPLOY_SSH_KEY Private key for that user

On the first deploy, a ~/moodtracker/config.json with a random invite code is generated and printed in the job log. The database lives in ~/moodtracker/data/ and survives updates.

The container only listens on 127.0.0.1:8081, so put a reverse proxy with HTTPS in front of it. Mood data is personal, and release builds of the app refuse plain HTTP. Caddy example:

mood.example.com {
    reverse_proxy 127.0.0.1:8081
}

The Android app

./deploy.sh                      # build + install debug build on the connected phone
./gradlew :app:assembleRelease -PserverUrl=https://mood.example.com   # signed APK in app/build/outputs/apk/release/

-PserverUrl prefills the server field on the login screen. Without it, the field starts as https://.

Release signing: release builds are signed with the key referenced by keystore.properties in the repo root (gitignored):

storeFile=/home/you/.android/moodtracker-release.jks
storePassword=…
keyAlias=moodtracker
keyPassword=…

Back up the keystore and this file. Android only installs an update over an existing install if it's signed with the same key. If you lose the key, everyone has to uninstall, which deletes their unsynced entries, and reinstall. Bump versionCode in app/build.gradle.kts for every APK you hand out.

Requires JDK 17 and the Android SDK (compileSdk 37). Debug builds use the .debug application id and allow plain HTTP, so the emulator can talk to http://10.0.2.2:8081.

Screens

  • Log: optionally type a note, then tap a face. That's it (Undo is in the snackbar).
  • Now: everyone's current mood, color-coded by category.
  • Compare: a step chart of everyone's mood over 24h / 7d / 30d / 90d, the share of time spent in each category, and a timeline. Long-press one of your own entries to delete it.
  • Settings: Gotify notifications, a local daily reminder, and logout.

Entries are saved locally first and synced when there's network. The app also syncs every ~15 minutes and whenever it is opened.

Gotify notifications

  1. In Gotify, go to Apps → Create Application (e.g. "Moodtracker") and copy its token.
  2. In the Moodtracker app, open Settings, enter your Gotify URL and that token, turn on notifications, and Save.
  3. Install the Gotify Android app and log in to the same Gotify server.

When a friend's newest entry lands in a different category than their previous one (e.g. neutral → negative), the server pushes 🌧️ name: neutral → negative, with their note as the message. You are never notified about your own entries. Entries logged offline that arrive later but are older than the newest one don't trigger a push.

API

All endpoints except /ping, register, login and logout need Authorization: Bearer <token>.

Method Path
POST /api/auth/register {username, email, password, invite_code} → {token, user_id, username}
POST /api/auth/login {identifier, password} → {token, user_id, username}
POST /api/auth/logout
GET /api/auth/me
POST /api/moods [{client_id, score, note, created_at}] → {synced, rejected}, idempotent on client_id
GET /api/moods?from=<ms> Everyone's entries since from, oldest first
DELETE /api/moods/:client_id Delete one of your own entries
GET /api/users Everyone with their latest entry
GET / PUT /api/settings {gotify_url, gotify_token, notify_enabled}