A Rails service of Medical Information System (MIS) that enables medical staff to create, manage, and track recurring and one-time tasks.
- Ruby 3.4+ — Main Programming language
- Rails 8.1 — Server-side web framework with MVC pattern
- PostgreSQL 16+ — Relational database with JSONB support
- Hotwire (Turbo + Stimulus) — Frontend-part
- Propshaft — Default static asset pipeline in Rails 8
- Docker/Podman + compose — Containerization and service orchestration
graph TD
subgraph "Client"
Browser[Web Browser]
end
subgraph "tracker-service"
API[API Controller<br/>JSON]
Web[Web Controller<br/>Hotwire]
Domains[Domain Services<br/>DDD Layer]
end
subgraph "Database"
DB[(PostgreSQL 16)]
end
Browser --> API
Browser --> Web
API --> Domains
Web --> Domains
Domains --> DB
| Service | Image | Port | Description |
|---|---|---|---|
| tracker-service | ruby:3.4.7 |
10078 |
Rails 8.1 task service |
| db-service | postgres:16.13 |
5432 |
PostgreSQL database |
-
Clone the repository:
git clone https://github.com/Mournweiss/task-tracker.git cd task-tracker -
Prepare the environment and build:
chmod +x build.sh ./build.sh
--podman|-p Use podman-compose instead of docker-compose --docker|-d Use docker-compose instead of podman-compose --foreground|-f Run in foreground mode --help|-h Show help messageNote:
build.shautomatically generates .env and selects an available orchestration engine if no specific option is given. To force a specific orchestrator, use the--podman/-por--docker/-dargument as needed. -
Access the application at
http://localhost:10078.
All API endpoints follow JSON:API conventions. Base URL: http://localhost:10078/api/v1. Full API specification is available in OpenAPI docs.
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/health |
Service health status |
Request:
curl http://localhost:10078/api/v1/healthResponse (200 OK):
{
"status": "healthy",
"timestamp": "2026-05-19T01:00:00Z",
"checks": {
"database": {
"healthy": true,
"message": "connected"
}
}
}Response (503 Service Unavailable):
{
"status": "unhealthy",
"timestamp": "2026-05-19T01:00:00Z",
"checks": {
"database": {
"healthy": false,
"message": "connection refused"
}
}
}| Method | Path | Description |
|---|---|---|
GET |
/api/v1/tasks |
List tasks (filters: status, from_date, to_date, tag_ids) |
POST |
/api/v1/tasks |
Create a task |
GET |
/api/v1/tasks/:id |
Get task by ID |
PATCH |
/api/v1/tasks/:id |
Update a task |
DELETE |
/api/v1/tasks/:id |
Delete a task |
GET |
/api/v1/tasks/calendar |
Get calendar data for date range |
List Tasks — Request:
curl "http://localhost:10078/api/v1/tasks?status=new&from_date=2026-05-01&to_date=2026-05-31&tag_ids=1,2"List Tasks — Response (200 OK):
{
"data": [
{
"id": 1,
"type": "tasks",
"attributes": {
"title": "Daily Patient Round",
"description": "Conward rounds on ward 3",
"scheduled_at": "2026-05-19T09:00:00Z",
"status": "new",
"recurrence_type": "daily",
"recurrence_step": 1,
"recurrence_day": null,
"recurrence_dates": null,
"recurrence_parity": null,
"end_at": null,
"created_at": "2026-05-18T12:00:00Z",
"updated_at": "2026-05-18T12:00:00Z"
},
"relationships": {
"tags": {
"data": [{ "id": 1, "type": "tags" }]
},
"task_instances": {
"data": []
}
}
}
]
}Create Task — Request:
curl -X POST http://localhost:10078/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"task": {
"title": "Prepare Monthly Report",
"description": "Compile statistics for the department",
"scheduled_at": "2026-06-01T10:00:00Z",
"status": "new",
"recurrence_type": "monthly",
"recurrence_day": 1,
"end_at": "2026-12-31T23:59:59Z"
},
"tag_ids": [1, 3]
}'Create Task — Response (201 Created):
{
"data": {
"id": 2,
"type": "tasks",
"attributes": {
"title": "Prepare Monthly Report",
"description": "Compile statistics for the department",
"scheduled_at": "2026-06-01T10:00:00Z",
"status": "new",
"recurrence_type": "monthly",
"recurrence_step": null,
"recurrence_day": 1,
"recurrence_dates": null,
"recurrence_parity": null,
"end_at": "2026-12-31T23:59:59Z",
"created_at": "2026-05-19T01:00:00Z",
"updated_at": "2026-05-19T01:00:00Z"
},
"relationships": {
"tags": {
"data": [
{ "id": 1, "type": "tags" },
{ "id": 3, "type": "tags" }
]
},
"task_instances": {
"data": []
}
}
}
}Get Task — Response (200 OK):
curl http://localhost:10078/api/v1/tasks/1{
"data": {
"id": 1,
"type": "tasks",
"attributes": {
"title": "Daily Patient Round",
"description": "Conward rounds on ward 3",
"scheduled_at": "2026-05-19T09:00:00Z",
"status": "new",
"recurrence_type": "daily",
"recurrence_step": 1,
"recurrence_day": null,
"recurrence_dates": null,
"recurrence_parity": null,
"end_at": null,
"created_at": "2026-05-18T12:00:00Z",
"updated_at": "2026-05-18T12:00:00Z"
},
"relationships": {
"tags": {
"data": [{ "id": 1, "type": "tags" }]
},
"task_instances": {
"data": []
}
}
}
}Update Task — Request:
curl -X PATCH http://localhost:10078/api/v1/tasks/1 \
-H "Content-Type: application/json" \
-d '{
"task": {
"status": "in_progress",
"recurrence_step": 2
}
}'Delete Task — Response (204 No Content):
curl -X DELETE http://localhost:10078/api/v1/tasks/1Calendar — Request:
curl "http://localhost:10078/api/v1/tasks/calendar?from_date=2026-05-19&to_date=2026-05-26"Calendar — Response (200 OK):
[
{
"task_id": 1,
"task_title": "Daily Patient Round",
"recurrence_type": "daily",
"instances": [
{
"date": "2026-05-19",
"task_id": 1,
"task_title": "Daily Patient Round",
"status": "new",
"notes": null,
"is_virtual": true
},
{
"date": "2026-05-20",
"task_id": 1,
"task_title": "Daily Patient Round",
"status": "new",
"notes": null,
"is_virtual": true
}
]
}
]| Method | Path | Description |
|---|---|---|
GET |
/api/v1/tasks/:task_id/instances |
List instances for a task |
POST |
/api/v1/tasks/:task_id/instances?occurred_at=YYYY-MM-DD |
Create/update instance |
PATCH |
/api/v1/tasks/:task_id/instances?occurred_at=YYYY-MM-DD |
Update instance status |
List Instances — Response (200 OK):
curl http://localhost:10078/api/v1/tasks/1/instances?from_date=2026-05-01&to_date=2026-05-31{
"data": [
{
"id": 1,
"type": "task_instances",
"attributes": {
"task_id": 1,
"occurred_at": "2026-05-19",
"status": "completed",
"notes": "Patient rounds completed",
"created_at": "2026-05-19T11:00:00Z",
"updated_at": "2026-05-19T14:00:00Z"
}
}
]
}Update Instance — Request:
curl -X PATCH "http://localhost:10078/api/v1/tasks/1/instances?occurred_at=2026-05-20" \
-H "Content-Type: application/json" \
-d '{
"status": "completed",
"notes": "Rounds completed on time"
}'| Method | Path | Description |
|---|---|---|
GET |
/api/v1/tags |
List all tags |
GET |
/api/v1/tags/search?q=string |
Search tags (autocomplete) |
POST |
/api/v1/tags |
Create a tag |
DELETE |
/api/v1/tags/:id |
Delete a tag |
DELETE |
/api/v1/tasks/:task_id/tags/:tag_id |
Unassign tag from task |
List Tags — Response (200 OK):
curl http://localhost:10078/api/v1/tags{
"data": [
{
"id": 1,
"type": "tags",
"attributes": {
"name": "reporting",
"system": true,
"created_at": "2026-05-01T00:00:00Z"
}
},
{
"id": 2,
"type": "tags",
"attributes": {
"name": "operations",
"system": true,
"created_at": "2026-05-01T00:00:00Z"
}
}
]
}Search Tags — Response (200 OK):
curl "http://localhost:10078/api/v1/tags/search?q=report"{
"data": [
{ "id": 1, "name": "reporting", "system": true },
{ "id": 4, "name": "reporting-urgent", "system": false }
]
}Create Tag — Request:
curl -X POST http://localhost:10078/api/v1/tags \
-H "Content-Type: application/json" \
-d '{
"name": "follow-up",
"system": false
}'Create Tag — Response (201 Created):
{
"data": {
"id": 5,
"type": "tags",
"attributes": {
"name": "follow-up",
"system": false,
"created_at": "2026-05-19T01:00:00Z"
}
}
}Unassign Tag — Response (204 No Content):
curl -X DELETE http://localhost:10078/api/v1/tasks/1/tags/2- SECRET_KEY_BASE: Secret key for session signing (Default:
generated) - RAILS_ALLOWED_HOSTS: Allowed connection hosts (Default:
*) - RAILS_ENV: Rails environment (Default:
production) - RAILS_LOG_TO_STDOUT: Log output to stdout (Default:
true) - RAILS_SERVE_STATIC_FILES: Serve static files (Default:
true) - LOG_LEVEL: Logging level (Default:
info) - MAX_LOG_SIZE: Maximum log file size in bytes (Default:
10485760)
- DB_HOST: Database host (Default:
db-service) - DB_PORT: Database port (Default:
5432) - DB_NAME: Database name (Default:
task_tracker) - DB_USER: Database user (Default:
postgres) - DB_PASSWORD: Database password (Default:
postgres)
- REGISTRY_TRACKER: Ruby image registry (Default:
docker.io/library/ruby) - VERSION_TRACKER: Ruby image version (Default:
3.4.7) - REGISTRY_DB: PostgreSQL image registry (Default:
docker.io/library/postgres) - VERSION_DB: PostgreSQL image version (Default:
16.13) - PROJECT_NAME: Container name prefix (Default:
task-tracker) - TRACKER_PORT: External port for tracker-service (Default:
10078)
The project implements full CRUD operations for tasks via the API:
- Create:
POST /api/v1/tasks— creates a task with title, description, scheduled_at, status, and optional recurrence settings - Read:
GET /api/v1/tasks(list with filters) andGET /api/v1/tasks/:id(single task) - Update:
PATCH /api/v1/tasks/:id— updates task attributes including status transitions - Delete:
DELETE /api/v1/tasks/:id— deletes task with cascade removal of instances and tags
Each task has at minimum: title, description, scheduled_at, and status fields.
Many-to-many tag relationships are implemented via the task_tags join table:
- Assign: Pass
tag_idsarray when creating/updating a task - Unassign:
DELETE /api/v1/tasks/:task_id/tags/:tag_id - Search:
GET /api/v1/tags/search?q=stringfor autocomplete
System tags (reporting, operations, call) are:
- Seeded automatically via migration
005_seed_system_tags.rb - Protected from deletion via
systemboolean flag +can_be_deleted?()check - Protected from renaming via
system_tag_name?()check inTag
The RecurrenceCalculator supports all required recurrence types:
| Type | Field | Description |
|---|---|---|
daily |
recurrence_step |
Every N days |
monthly |
recurrence_day |
On specific day (1-31) |
specific_dates |
recurrence_dates (JSONB) |
On explicit dates |
even_odd |
recurrence_parity |
Even/odd days only |
Infinite recurring tasks (without end_at) are handled via lazy computation:
- The
taskstable stores only the rule (recurrence configuration), not pre-generated instances - Instances are computed on-the-fly when requesting calendar data via
CalendarService - The
RecurrenceCalculatorexpands only instances within the requested[from_date, to_date]window - Maximum computation window is bounded to prevent excessive processing
Each instance has its own independent status:
- The
task_instancestable stores overrides (status changes, notes, cancellations) - Unmodified instances are virtual — they inherit status from the parent task
effective_statusmethod returns the instance's own status or inherits from parent- Marking one instance as completed does not affect other instances
Instances can be modified independently from the parent task rule:
statusfield intask_instancesallows overriding the inherited statusnotesfield stores instance-level notesPATCH /api/v1/tasks/:task_id/instances?occurred_at=YYYY-MM-DDupdates an instance- An instance with a non-null
statusis considered an "exception" from the rule
This project is licensed under the Apache License 2.0.
| Component | License | Source |
|---|---|---|
| Ruby | Ruby License | ruby/ruby |
| Rails | MIT | rails/rails |
| PostgreSQL | PostgreSQL License | postgres/postgres |
| Turbo | MIT | hotwired/turbo |
| Stimulus | MIT | hotwired/stimulus |
| Propshaft | MIT | rails/propshaft |
| jsonapi-serializer | MIT | jsonapi-serializer/jsonapi-serializer |
| Puma | BSD License | puma/puma |
| bootsnap | MIT License | Shopify/bootsnap |
| RuboCop | MIT | rubocop/rubocop |
| Docker | Apache 2.0 | docker/docker |