English | 日本語
A clinical ledger application for a single hospital department, tracking patients, diagnoses, surgeries, and hospitalizations. It brings outpatient and ward records together in one place, with a focus on making elective slot utilization, the lifecycle of a hospitalization from request to discharge, and an audit log of every change visible under per-user permissions. The landing page is an operations calendar spanning 50 days by default, showing daily surgery and admission counts, congestion warnings, holidays, and announcements at a glance.
It is built entirely on the stock Rails 8.1 stack (Propshaft + importmap + Turbo/Stimulus) — no Node build pipeline and no external database server.
Table of contents
- Features
- Tech stack
- Setup
- Configuration
- Development commands and testing
- Domain model
- Design notes
- Deployment
- Development rules
- License
Features
| Feature | Screen | Description |
|---|---|---|
| Login | /login |
Login id (email or username, per ACCOUNT_IDENTIFIER) + password authentication. Sessions expire automatically after a period of inactivity. Published announcements are shown on this page, and it is displayed in Japanese for anonymous visitors (a language switcher sits on the page) |
| Operations calendar (home) | /operations_calendar |
Day-grouped ledger table spanning 50 days by default (start date and day count adjustable). Per day: surgeries by slot with the referral source, admission counts, congestion warnings, special slot usage, and holidays / per-day comments. The date heading and a small "+入約" link open the reservation form pre-filled with that day; each surgery row of a hospitalization has an edit link to the same form; two tabs, 手術 (surgeries) and 検査入院 (non-surgery admissions by scheduled procedure/admission date, columns from ledger_columns.examination_calendar), selected with ?view=; emergency rows carry a red stripe and rows outside the regular slot time are hatched (legend above the table, kept in print); administrators edit a day's note/closure/slot override inline (/day_notes/:date) and that day's special slot lanes through the day cell's "Edit special slots" link (/day_special_slots/:date); filters 自分が関与する症例のみ (mine=1) and, with features.teams, a team select (team_id) are kept across tabs and ranges |
| Patient ledger | /patients |
Patient master data, with keyword search by name or hospital ID and pagination |
| Patient diagnoses | /patients/:id/patient_diagnoses |
Per-patient diagnosis history referencing the diagnosis master, holding the diagnosis date and laterality |
| Surgery records | /surgeries |
Surgery date (may be "undecided"), procedures (up to 5), anesthesia method, duration, elective/emergency type, slot category (regular / joint / backup / off-slot), operator/assistant/operation order, location / device / partner and performing department. Linked to patient diagnoses and, optionally, to the hospitalization it belongs to. Searchable by operator |
| Hospitalization records | /hospitalizations |
Lifecycle from reservation (scheduled admission and surgery dates) through actual admission and discharge, admission purpose (surgery / examination / chemotherapy / chemoradiotherapy / other, the last requiring a reason), emergency admission, transfer status, reservation doctor, referral source, administrator confirmation, soft delete/restore, and rebooking (copy) |
| Reservation form (入約) | /reservations/new, /reservations/:hospitalization_id/edit |
One screen that creates or edits a hospitalization and, for the purpose surgery, its surgery together (see Reservation form). The calendar's booking flow goes through it; the separate hospitalization and surgery forms remain for administration and detail edits |
| Surgery slot schedule | /surgery_schedule |
Weekly calendar showing the surgeries and used time per slot, warning about slot overruns, unassigned surgeries, and holidays. The same week can be printed as a landscape A4 PDF (/surgery_schedule/pdf?start_date=YYYY-MM-DD, any start date) in black and white, with operator/assistant, referral source, and a heading per day (date, closed-day name / note, slot count when overridden, slot usage in minutes, case count, special slot usage); emergency rows are shaded and rows outside the regular slot time lightly shaded (legend under the title) |
| Data download (admin / data manager only) | /exports |
CSV exports defined in the site config, with a date range (and team) per export (see CSV export) |
| Dashboard | /dashboard |
Statistics filterable by year (patient counts, inpatients, monthly counts, average length of stay, procedure ranking) |
| Cross-entity search | /search |
Search patients, hospitalizations, and surgeries with a single keyword; results are split into 入院 / 手術 / 患者 tabs (?tab=, each with its match count; without one, the first tab with matches opens) and listed as tables of up to 20 rows with a link to the full list |
| Audit log | /audit_events |
Records and displays create/update/delete of patients, surgeries, hospitalizations, and case databases with the operator, IP address, and before/after values |
| Master data | /diagnoses /surgery_procedures /elective_slot_rules /holidays /departments /surgery_locations /surgical_devices /teams |
Diagnosis names, procedures, per-weekday slot rules (fractional slot counts supported), holidays / per-day comments and slot overrides, departments, surgery locations and surgical devices (the latter two with weekly special slot lane counts), and teams with their terms and members (/teams only with features.teams) |
| Case databases | /case_databases |
Named patient registries with custom fields (text / number / select) defined by a data manager. Any signed-in user can add patients, enter values, and export the registry as CSV; only a data manager can create, edit, or delete a database, its fields, or a patient entry |
| Account settings | /account |
Change your own name, login id, and password, and switch the display language |
| User management (admin only) | /admin/users |
Create, edit, and deactivate users and reset passwords. The last active admin can be neither deactivated nor demoted |
| Bulk user registration (admin only) | /admin/user_import/new |
Register users from a CSV of login ids and passwords. Existing login ids are skipped, and every line is reported back as registered / skipped / failed |
| Announcement management (admin only) | /admin/announcements |
Create announcements shown on the sign-in page and toggle their visibility |
| Admin notes (admin only) | /admin/admin_notes |
Free-text handover notes shared between administrators |
| Application settings (admin only) | /admin/settings |
Change the application title shown in the navigation bar, the browser tab, and the installable app |
Master data is edited by administrators only. Diagnoses, surgery procedures, holidays (closed
days / day notes / slot overrides), elective slot rules, departments, surgery locations, surgical
devices, teams and team terms can be viewed by every signed-in user, but creating, editing and deleting
them (and the "new" buttons) require the admin role; others are redirected with a "not authorized"
notice. The one exception is data entry: any signed-in user may register a new diagnosis or surgery
procedure from inside a picker modal (a turbo-frame request) while booking a hospitalization or surgery.
Reservation form. The calendar books through one form object (Reservation) that saves a
hospitalization and, for the purpose "surgery", its surgery in a single transaction, with live
previews of the patient, admission counts, slot usage and team members next to the form. Details are
under Reservation form in the design notes.
Authentication, user management, role separation (user / data manager / admin), access logging, and
idle timeout are implemented. Every application screen requires a logged-in user; the
exceptions are /login, the language switcher (PATCH /locale), and the /up health check.
Tech stack
| Area | Technology |
|---|---|
| Language / framework | Ruby 4.0.7 / Rails 8.1 |
| Database | SQLite (four schemas: the app itself, Solid Queue, Solid Cache, Solid Cable) |
| Background processing | Solid Queue / Solid Cache / Solid Cable |
| Assets | Propshaft + importmap-rails (no Node, no JS bundler) |
| CSS | Tailwind CSS + daisyUI (tailwindcss-rails) |
| Front end | Hotwire (Turbo Drive / Turbo Streams / Stimulus) |
| Testing | Minitest + fixtures; system tests with Capybara + Selenium |
| PDF output | Prawn + prawn-table (weekly surgery schedule; bundled IPAex Gothic font in vendor/fonts) |
| Static analysis | RuboCop (rubocop-rails-omakase), Brakeman, bundler-audit, importmap audit |
| Deployment | Kamal + Thruster (Dockerfile included) |
Setup
Prerequisites: Ruby 4.0.7 (see .ruby-version) and Bundler.
git clone https://github.com/studiome/altocumulus.git
cd altocumulus
bin/setup
bin/setup installs gems, creates/migrates/seeds the database, clears logs, and then
starts the development server. To prepare everything without starting the server:
bin/setup --skip-server
After that, use bin/dev (via Procfile.dev it runs the Rails server and the Tailwind
watcher together).
bin/dev
The app is available at http://localhost:3000. The root path is the operations calendar
(OperationsCalendarController#index); unauthenticated visitors are redirected to the
login screen.
In development, bin/setup also runs the seeds, so you can log in with the following
demo account (see db/seeds.rb), which holds both the admin and data_manager roles. The sign-in
screen is shown in Japanese for visitors who have not signed in; switch the language on the sign-in
page itself, or after logging in from the Account page (/account).
| Login ID | Password |
|---|---|
admin@example.com |
password |
The seeds contain only this account and the masters. To fill a development database with fictional
patients, hospitalizations and surgeries for UI review, run bin/rails dev:demo_data (see
Development commands and testing and
Development demo data).
For production, the first administrator and the other runtime settings are configured through environment variables and a site configuration file; see Configuration.
Configuration
Environment variables
Runtime behavior is tuned with environment variables (config/application.rb, db/seeds.rb).
| Environment variable | Description | Default |
|---|---|---|
ACCOUNT_IDENTIFIER |
Whether an account logs in with an email address or a username. Decide this when first setting up the server and do not change it afterward -- switching modes later makes existing login ids fail the new format validation. |
email |
SESSION_IDLE_TIMEOUT_MINUTES |
Session idle timeout, in minutes | 10 |
ADMISSION_WARNING_THRESHOLD |
Admissions per day above which the operations calendar flags congestion | 5 |
APP_TITLE |
Application title used before an administrator sets one on /admin/settings. Useful for branding a fresh deploy without signing in first |
Altocumulus |
SITE_CONFIG_PATH |
Path of the site configuration YAML (see Site configuration). In production, mount the file with a Kamal volume | config/site.yml |
BOOTSTRAP_ADMIN_LOGIN_ID |
Login id (email or username, depending on ACCOUNT_IDENTIFIER) of the first administrator |
(unset) |
BOOTSTRAP_ADMIN_PASSWORD |
Password of the first administrator | (unset) |
Setting both BOOTSTRAP_ADMIN_LOGIN_ID and BOOTSTRAP_ADMIN_PASSWORD on first boot creates a
bootstrap administrator account (named Administrator) when db:seed runs. If a user with the same
login id already exists, nothing happens. BOOTSTRAP_ADMIN_EMAIL is kept as an older name for
BOOTSTRAP_ADMIN_LOGIN_ID: it is still read as a fallback when that is unset. With Kamal these
variables must be listed under env: in config/deploy.yml -- see
Before the first deploy.
Site configuration
Per-hospital customizations are not hardcoded; they live in a YAML file loaded once at boot by
SiteConfig (app/models/site_config.rb). The file is the one at SITE_CONFIG_PATH if that
environment variable is set (in production, mount it with a Kamal volume -- see the commented
example in config/deploy.yml), otherwise the committed config/site.yml, a neutral default that
keeps the stock behavior. config/site.example.yml shows every key filled in. A malformed file, an
unknown key or an unknown field name stops the boot with a SiteConfig::Error. (Field names are not
checked while migrations are pending, so db:prepare can boot and migrate first; the server's boot
afterwards checks them.)
Keys:
features.teams(defaultfalse): turns the care-team feature on (see Teams). Whilefalse, the Teams screens, the team selects and the team displays are hidden andteam_idmay not be listed underrequired_fields(the boot fails if it is).required_fields.hospitalization/required_fields.surgery: attribute names that must be filled in before saving. The red asterisk on the form follows the same list (Model.required_attribute?).required_fields.hospitalization_by_purpose: the same, but only for hospitalizations of one purpose (surgery,examination,chemotherapy,chemoradiotherapy,other), e.g.examination: [scheduled_procedure_date, procedure_name]. The asterisk follows the purpose chosen in the form (Hospitalization.required_attribute?(attr, record)or(attr, purpose: ...)). The reservation doctor is the user referencereservation_doctor_id(not the oldreservation_doctorstring).patient.hospital_id_format/patient.hospital_id_format_hint: a regular expression the patient's hospital ID must match when it is set or changed, and a human-readable description for the error message. Omit for no restriction.masters.departments/masters.surgery_locations/masters.surgical_devices: lists of names (for example a hospital's departments, its rooms such as a hybrid room or cath lab, and its support devices) to make sure exist in the master tables. Nothing hospital-specific is hardcoded in the app; the vocabulary lives in those admin-editable masters (Masters menu), and this key only seeds them. Runbin/rails site:seed_mastersafter a deploy (it is also part ofdb:seed): it only creates missing names, in the listed order after the existing rows, and never renames, reorders or deletes anything. A deactivated master is no longer offered for new selections but still shows on the surgeries that use it; a master in use cannot be deleted. Asurgery_locationsentry may also be a mapping,{name: ..., asks_performing_department: true}, which flags a place whose cases may be performed by another department (a hint for the surgery form; set only on newly created rows, and editable on the Surgery Locations master screen). Special slot lane counts are not part of this file; they are edited on the master screens.masters.teams: team names (or{name: ..., color: blue}, color one of blue/red/green/orange/purple/ teal/gray), seeded the same additive way as the other masters. Terms and members are entered in the app, not in this file.ledger_columns.operations_calendar/weekly_pdf/examination_calendar: the columns each ledger table shows, in order (the calendar's and PDF's day cell is always first and not listed). Columns come from the ledger column registry (LedgerColumn,app/models/ledger_column.rbandapp/models/ledger_columns/): surgery ledgers usetime_slot,admission_dates,patient,age_sex,diagnosis_procedure,operator_assistant,duration_anesthesia,team(needsfeatures.teams),category,reservation,referral_adl,status; the examination ledger usesadmission_dates,patient,age_sex,diagnosis_procedure_name,purpose,team,category,reservation,referral_adl,status. Omit a ledger for its built-in default. Unknown or duplicate columns, an empty list, orteamwithoutfeatures.teamsstop the boot. Seeconfig/site.ymlfor the column meanings.weekly_pdf:font_size(6-10, default 8) andtitle(replaces the translated PDF title; keep site-specific wording here, not in code). Columns have a preferred and a minimum width in the registry: spare page width is shared out by each column'sflexweight (category 3, team 2, diagnosis/procedure 2, referral/ADL 1.5, patient, reservation and status 1; others 0), not by width, so the columns that wrap most grow most; a too wide set shrinks towards the minimums, and only then does the font step down (8 -> 7.5 -> 7 -> 6.5) before a final proportional squeeze (LedgerColumnFitting).exports: named CSV exports for the clerk's admission list / registry (NCD) lists. Each entry is<name>: {subject: surgery|hospitalization, fields: [...], label: optional}; fields come from the export field registry (ExportField,app/models/export_field.rbandapp/models/export_fields/), which yields one plain value per column (unlike the stacked ledger cells). Omit the key for the built-insurgery_listandadmission_list(every field of the subject);exports: {}switches exports off. Unknown subjects/fields, duplicates, an empty list, a bad name orteam/team_memberswithoutfeatures.teamsstop the boot. Seeconfig/site.ymlfor every field andconfig/site.example.ymlfor anncd_list.
In tests, with_site_config(hash) { ... } swaps the config temporarily.
Development commands and testing
bin/dev # dev server + Tailwind watcher
bin/rails test # all tests except system tests
bin/rails test test/models/patient_test.rb # a single file
bin/rails test test/models/patient_test.rb:12 # a single test at a line
bin/rails test:system # system tests (requires Selenium)
bin/rails db:prepare # create / migrate / seed the database
bin/rubocop # lint
bin/brakeman # static security analysis
bin/bundler-audit # audit gems for known vulnerabilities
bin/ci # the same checks CI runs (config/ci.rb)
bin/ci runs setup → RuboCop → the gem/importmap/Brakeman audits → tests → a re-run of
the seeds. GitHub Actions (.github/workflows/ci.yml) runs an equivalent job on pushes
to main and on pull requests.
Rake tasks (lib/tasks/):
| Task | Purpose |
|---|---|
bin/rails site:seed_masters |
Creates the departments, surgery locations, surgical devices and teams listed under masters in the site config. Additive and idempotent: it never renames, reorders or deletes existing rows. Also run from db/seeds.rb |
bin/rails dev:demo_data |
Development only. Fills the database with fictional, idempotently created demo data for UI review. REFRESH=1 also rewrites the clinical content of the demo cases in place (see Development demo data) |
bin/rails surgeries:move_special_off_slot_to_regular |
Moves elective off-slot surgeries that use a special slot resource (hybrid room, ca |
