Tracker is an at-con time clock system for volunteers at BLFC. Volunteers can clock in and clock out for their shifts to log their hours, ensuring they get the rewards they deserve. Staff can manage volunteers and run reports on volunteer hours.
Tracker is an evolving system and has many planned features and improvements on their way, but here is a semi-comprehensive list of the current features implemented.
Docker is recommended to run Tracker, as a Docker Compose file and corresponding Dockerfiles are already built to make the process as easy as possible. Certbot-based Let's Encrypt automatic SSL renewal support is provided out-of-the-box with the default production Compose file.
*.env.example
files in .docker/env
to just *.env
(in the same directory).
.docker/env
should have app.env
, certbot.env
, nginx.env
, postgres.env
, and redis.env
..env
files in .docker/env
for your configuration.
Specifically, you should ensure the following keys are updated at the very least:
APP_KEY
, APP_URL
, DB_PASSWORD
, REDIS_PASSWORD
, CONCAT_CLIENT_ID
, CONCAT_CLIENT_SECRET
, TELEGRAM_BOT_TOKEN
(see details in the development section for the ConCat and Telegram config keys, as well as for generating a key for APP_KEY
the first time)LETSENCRYPT_DOMAIN
, LETSENCRYPT_EMAIL
, LETSENCRYPT_DRY_RUN
(clear the value for this once confirmed working)NGINX_HOST
POSTGRES_PASSWORD
(should match DB_PASSWORD
in app.env
)REDIS_PASSWORD
(should match REDIS_PASSWORD
in app.env
)docker compose -f docker-compose.prod.yml build
to build the necessary (app and nginx) imagesREDIS_PASSWORD=<redis password here> docker compose -f docker-compose.prod.yml up -d
to run the images in the Docker daemon
REDIS_PASSWORD
is sadly currently required to start the Redis container properly due to the way the variable is obtaineddocker compose -f docker-compose.prod.yml exec app php artisan migrate
to run migrations on the databasedocker compose -f docker-compose.prod.yml logs certbot
to confirm that the dry-run succeededCERTBOT_DRY_RUN
in certbot.envdocker compose -f docker-compose.prod.yml restart certbot
docker compose -f docker-compose.prod.yml exec app php artisan auth:set-role
to set your user's role to adminA convenient update script is provided at /.docker/scripts/update.sh. In this order, that script does the following:
git pull
)docker compose -f docker-compose.prod.yml build
)docker compose -f docker-compose.prod.yml down && docker compose -f docker-compose.prod.yml up -d
)docker compose -f docker-compose.prod.yml exec app chown -R www-data:www-data /var/www/html/storage
)docker compose -f docker-compose.prod.yml exec app php artisan down
)docker compose -f docker-compose.prod.yml exec app php artisan migrate
)docker compose -f docker-compose.prod.yml exec app php artisan up
)These steps can be completed manually if preferred or if any tweaking is desired.
Whenever the Postgres major version is updated in the Compose file, Postgres needs to be manually upgraded beforehand. If not done, an error like this will likely be encountered at the startup of the Postgres container:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 15, which is not compatible with this version 16.1 (Debian 16.1-1.pgdg120+1).
Reverting the project (or at least the Compose file) to the previous version should allow the server to start again. Follow this procedure to properly upgrade the database (starting with the server running the old version):
./docker/scripts/postgres-dump.sh
to dump the contents of the database to pgdump.sql
in the project directorydocker compose -f docker-compose.prod.yml stop postgres
)docker volume ls
- it should be something like projectname_postgres
docker volume rm postgres_volume_name
)docker compose -f docker-compose.prod.yml start postgres
)./docker/scripts/postgres-restore.sh
to import pgdump.sql
into the new Postgres versionoauth:manage
permission.
Alternatively, have someone else create an OAuth app in ConCat for you and have them provide you the client ID and secret.volunteer:read
and registration:read
application permissions for OAuth Bearer tokens, which are used for generating the Volunteer Applications reports and retrieving badge details inside Tracker.composer install --no-dev --classmap-authoritative
to download all production backend dependencies and optimize the autoloader automatically.npm install
to download all frontend dependencies.npm run build
to bundle and optimize the frontend assets..env.example
to .env
and update the values appropriately.php artisan key:generate
to generate an encryption key and automatically fill in the APP_KEY
value in .env
.
This key should be kept the same between all instances of Tracker connected to the same environment (production, QA, etc.) and should only be regenerated when absolutely necessary (compromise, improved algorithm).
Regenerating or using a different key will result in any encrypted (not hashed!) values in the database or cache becoming unreadable.php artisan migrate
to run all migrations on the database.php artisan auth:set-role
to set your user's role to admin.php artisan telegram:set-commands
to send the list of bot commands to Telegram.php artisan telegram:set-webhook
to inform Telegram of the bot's webhook URL.php artisan schedule:run
every minute so that reward notifications can be triggered and ongoing shifts automatically stopped at the configured day boundary.
* * * * * cd /var/www/html && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1'
php artisan queue:work
in a separate process (using supervisor or something similar) to process queue entries as they come in.
You can have multiple of these running at once if the queue becomes backed up.php artisan config:cache
to cache the fully-resolved configuration to a filephp artisan route:cache
to cache the routes to a filephp artisan event:cache
to cache the auto-discovered event listeners to a filephp artisan view:cache
to pre-compile and cache all of the Blade templatesphp artisan down
to put the application in maintenace mode.composer install --no-dev --classmap-authoritative
to download any new production backend dependencies and optimize the autoloader automatically.npm install
to download any new frontend dependencies.npm run build
to bundle and optimize the frontend assets.php artisan migrate
to run any new migrations on the database.php artisan telegram:set-commands
to send the list of bot commands to Telegram.php artisan telegram:set-webhook
to inform Telegram of the bot's webhook URL.php artisan queue:work
) in separate processes to ensure they're using the latest code.php artisan config:cache
to cache the fully-resolved configuration to a filephp artisan route:cache
to cache the routes to a filephp artisan event:cache
to cache the auto-discovered event listeners to a filephp artisan view:cache
to pre-compile and cache all of the Blade templatesphp artisan up
to pull the application out of maintenace mode.The development environment uses Laravel Sail, a containerized environment with a script and easy-to-use commands for interacting with it.
Copy .env.example
to .env
:
cp .env.example .env
After doing so, update the values only as needed. The important ones that will most likely need to be filled in are the ConCat and Telegram items.
Housekeeping
-> Developers
-> OAuth Applications
-> Create New
http://localhost
for the callback URLregistration:read
and volunteer:read
application permissionsCONCAT_CLIENT_SECRET
and CONCAT_CLIENT_ID
in .env
/newbot
)TELEGRAM_BOT_TOKEN
in .env
sudo apt install php-cli
)composer install
in the application directorysail
alias with alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
~/.bashrc
)Whenever using a Sail command, if you don't have an alias setup, use sh vendor/bin/sail
instead of sail
.
sail up
(Ctrl+C to stop)sail artisan key:generate
(Updates APP_KEY
in .env
)sail npm install
sail artisan migrate
sail artisan db:seed
sail npm run dev
(Ctrl+C to stop)php artisan tinker
or sail artisan tinker
will present a PHP REPL with the application bootstrapped, allowing you to mess with any part of the application and see the result of code in real-time. See the Artisan documentation for more information.dump(...)
and dd(...)
can be extremely helpful for debugging the application. The former pretty-prints a representation of any data passed to it with full HTML formatting, and the latter does the same but also immediately halts further execution of the application. Collections and Carbon instances also have ->dump()
and ->dd()
methods.php artisan make:migration
or sail artisan make:migration
to create a new database migration. See the migrations documentation for more information.composer run format
and npm run format
will format all PHP and JavaScript code, respectively.npm run lint
will lint all JavaScript code, checking for common errors and making recommendations.
npm run lint:fix
will automatically apply fixes for many of these.npm run lint:fix-unsafe
will correct even more, but these changes should be manually verified.Since Laravel is an MVC (Model, View, Controller) framework, that structure is generally adhered to. PSR-4 autoloading is in use, so as long as the namespace and class filesystem structure is followed, files don't need to be manually included/required.
A significant rework of the frontend is underway to modernize it by rebuilding it with Vue & TypeScript, using Inertia to connect it to the backend. At the moment, the only page that has been fully rebuilt is the Manager Controls page.
Use php artisan help
or sail artisan help
to view a list of all available commands, not just custom ones.
Use php artisan help <command name>
or sail artisan help <command name>
to view detailed information for a specific command.
Name | Description |
---|---|
auth:set-role | Sets the role for a user. If user isn't specified on the CLI, then you will be prompted to search for and select the appropriate user, as well as to select the role to assign. |
auth:fetch-unknown | Retrieves and populates information for any users with the unknown username (previously-created Users that information couldn't be retrieved from ConCat for at the time). |
tracker:notify-rewards | Sends notifications to users for rewards they are newly eligible to claim. Automatically called by the task scheduler every 5 minutes. |
tracker:stop-ongoing | Stops all ongoing time entries for the active event. Automatically called by the task scheduler every day at the configured day boundary hour. |
telegram:set-commands | Sends the list of commands to Telegram to display to users interacting with the bot. |
telegram:set-webhook | Sends the webhook URL to Telegram. Requires the application to be accessed via HTTPS. |
telegram:poll | Polls Telegram for updates (primarily for development use). |
All database models are using UUIDv7 for their primary key (id
column).
Eloquent is being used heavily for nearly all database interactions.
Foreign key constraints are used in the database whenever possible to ensure referential integrity at every step of the process.
All models and their relationships are listed below, alongside a brief description of their purpose.
Name | Table | Description |
---|---|---|
Activity | activities | Used for tracking events and changes to models for audit logging purposes. Belongs to a User via both subject and causer. |
AttendeeLog | attendee_logs | A log to enter users into. Used for tracking attendance to a panel or other type of event. Has many Users, with type (attendee or gatekeeper ) on the pivot table. Belongs to an Event. |
Department | departments | An organizational unit for staff/volunteers of a convention. |
Event | events | A single convention/other type of event that time is tracked for. |
Kiosk | kiosks | A device that has been authorized to allow volunteers to enter time on. These devices keep a cookie with the session key to identify themselves. |
QuickCode | quick_codes | One-time-use sign-in codes for users. Expires 30 seconds after creation. Belongs to a User. |
Reward | rewards | Possible reward/goal for volunteers to thank them for their time once they reach a threshold. Belongs to an Event. |
RewardClaim | reward_claims | Rewards claimed by users. Belongs to a User and a Reward. |
Setting | settings | Application settings identified by a string and stored as JSON. |
TimeBonus | time_bonuses | Time periods that grant bonus volunteer time credit while being worked within. Belongs to an Event and many Departments. |
TimeEntry | time_entries | Volunteer time clocked by users. Belongs to a User, a Department, and an Event. |
User | users | Any user of the application. Has a role (Banned, Attendee, Volunteer, Staff, Lead, Manager, Admin) that determines permissions. |
Permissions are implemented very simply at the moment. Users have a single assigned Role value, which is just an enum (Banned, Attendee, Volunteer, Staff, Lead, Manager, Admin).
Since the primary purpose of Tracker is to track the time that volunteers spend working shifts, a lot of care has been put in to how that time is kept.
In order for a volunteer to enter time, they must visit an authorized kiosk at the beginning and end of their shift.
When they clock in, they select a department the shift will be for, and a TimeEntry is created in the database with the current time as its start
field and a null stop
field.
Any TimeEntry with a null stop
field is considered to be an "ongoing" entry.
An ongoing TimeEntry's duration is the amount of time between its start
and the current time.
Upon clocking out, the ongoing TimeEntry is updated to fill in the stop
field with the current time.
A volunteer may only ever have a single ongoing TimeEntry.
An admin can create multiple TimeBonuses for an Event that apply within a time period (between start
and stop
) to specific Departments.
Any TimeEntry that is assigned to a Department + Event with a TimeBonus and has its time range even partially within a TimeBonus period has the TimeBonus' multiplier applied to the amount of time that is within the bonus period.
Example scenario:
The TimeEntry's raw duration will be 3hrs, and its "earned time" (duration with bonuses) will be 4hrs. This is because the TimeEntry contained 1hr of time within the bonus period, so that single hour is considered to be worth double time.
Each day at the configured day boundary hour (see [config/tracker.php], default 04:00), the application has a scheduled task to automatically terminate any ongoing time entries. This is to catch the cases where a volunteer forgets to clock out after their shift, thus leaving the time entry running perpetually. When a time entry is terminated this way, its stop time is updated to be either 1hr after the start time or the current time, whichever is sooner. A notification is sent that they're forced to acknowledge on the web page the next time they log in.
Attendee logs are an entity used to track attendees for a scheduled event such as a panel or meetup. They can have any number of users entered into them by badge ID, and they don't even require the users entered to be valid volunteers or staff. Any number of Gatekeepers can also be added to them, who will all be able to view and manage the attendees that are logged, regardless of their own role.
Users can link their Telegram account to the application by scanning a QR code on the web page or manually visiting the proper Telegram link generated on it.
The QR code simply directs them to the same aforementioned link, which should automatically open Telegram and send a /start <setup key>
command to the bot.
Every user has a Telegram setup key that is unique to them and randomly generated upon creation.
When the bot receives the start command with their setup code, it stores the Telegram chat ID on their User and uses this for future communication.
The bot will automatically send notifications to users with linked accounts for the following scenarios:
When a user sends a message of any kind to the Telegram bot, Telegram contacts the configured webhook (php artisan telegram:set-webhook
) to notify the application of the message.
The message is checked for a valid command - if there is one, the command is processed.
All Telegram commands are in app/Telegram/Commands.
Telegram needs to be informed of these commands with php artisan telegram:set-commands
so that it may present a convenient list to users of the bot.
During development, php artisan telegram:poll
can be used instead of the webhook, which will start a long-term polling process to pull updates from Telegram rather than it pushing to the application.
This allows the Telegram bot to be tested without needing the application to be externally accessible to the internet.