- HTML 34.5%
- JavaScript 31.8%
- Ruby 22.2%
- CSS 10.6%
- SCSS 0.6%
- Other 0.2%
|
Edward Loveall
c8fdad00c4
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
`TrackerTotals` is similar to `RecentTaskLogTotals`. It sums and groups task log totals by month and task, instead of `RecentTaskLogTotals` which does it by month and food taxonomy. For styling, I reused the sites table because it looks similar to the list-table styles, but can work better with variable columns. Because we're using tasks as the columns, these tables can get _large_. So I also wrapped the table in a scrolling container. There's a nice side benefit that we get that we can make a minimum column width (only if `table-layout: auto;`) so that they don't get squished too much. |
||
|---|---|---|
| .woodpecker | Upgrade to ruby 4.0.5 | |
| app | Show monthly/task totals on tracker show page | |
| bin | Instructions for adding demo data to acceptance | |
| config | Show monthly/task totals on tracker show page | |
| db | Show monthly/task totals on tracker show page | |
| docs | Add some info about how to get raw server logs | |
| lib | Trigger CI cleanup on every CI run | |
| log | Raygun-zapped skeleton. | |
| public | Update landing page | |
| spec | Show monthly/task totals on tracker show page | |
| vendor/assets | Raygun-zapped skeleton. | |
| .env.sample | Fix URL host | |
| .eslintrc.json | Prettier | |
| .gitignore | Update README to match reality | |
| .gitmodules | Remove gitlab references | |
| .node-version | Upgrade to webpacker 6.0.0.rc6 | |
| .nvmrc | Specify node version | |
| .prettierignore | Prettier | |
| .rspec | Always use format describe | |
| .rubocop.yml | Prettier | |
| .rubocop_todo.yml | Prettier | |
| .ruby-gemset | Raygun-zapped skeleton. | |
| .ruby-version | Upgrade to ruby 4.0.5 | |
| .standard.yml | Install SolidQueue | |
| babel.config.js | Prettier | |
| Brewfile | Clean up old heroku references | |
| Caddyfile | Update README to match reality | |
| config.ru | standardrb --fix | |
| docker-compose.yml | Upgrade to ruby 3.4 | |
| esbuild.config.mjs | Migrate inline scripts to assets & rework shift event form logic | |
| Gemfile | Add rails-i18n and tweak settings | |
| Gemfile.lock | Upgrade dotenv/dotenv-rails to next major version | |
| Guardfile | standardrb --fix | |
| LICENSE | Switch to AGPLv3 | |
| NEWS.md | Prep release 2026年06月30日 | |
| package.json | Upgrade to react 19 | |
| Procfile.dev | Install SolidQueue | |
| Rakefile | Raygun-zapped skeleton. | |
| README.md | Fix URL host | |
| setupJest.js | Upgrade react-router to v7 | |
| yarn.lock | Update the language after the user submits their profile | |
Rootable
Licensing
This repository uses the AGPLv3 license. See the LICENSE file for full text. If you would like to purchase a license for commercial, closed source use, contact software@foodrescuealliance.org.
Development
First Time Setup
Requirements
To run the specs or fire up the server, be sure you have these installed (and running):
- Ruby (see .ruby-version). (
asdf install ruby) - Node.js 22.17.1 (see .node-version). (
asdf install nodejs) - PostgreSQL (installing with Postgres.app is recommended)
- External Dependencies (
brew bundle, see Brewfile)
bin/setup
Run ./bin/setup to install missing gems and prepare the database.
Note, rails db:sample_data (run as part of setup) loads a small set of data for development. Check out
db/sample_data.rb for details.
Then run seeds:
bundle exec rake db:seed
Running the Application Locally
The easiest way to run the app is using yarn start. This starts all the processes defined in Procfile.dev, including the Caddy reverse proxy, Rails server, and the asset recompilation.
yarn start
The app will then be accessible at https://rootable.localhost.
If you'd like to setup your own custom Overmind config, create Procfile.local (ignored by git) and set it up your way. Then, tell Overmind you'd like to use this by default in a .overmind.env file:
OVERMIND_PROCFILE=Procfile.local
Run overmind start instead of yarn start to run the processes.
Building assets separately
We use esbuild to build assets locally.
In production environments, the app builds the assets with rake assets:precompile, which eventually calls esbuild via node esbuild.config.mjs.
In local environments, you can use yarn build to build once, or yarn build --watch to have assets rebuilt automatically when files change.
.env
The bin/setup script will create a .env file that defines settings for your local environment. Do not check this into source control. Refer to the environment variables section below for what can be specified in .env.
Sending mail
In production we use Sendgrid. In development we use mailcatcher.
gem install mailcatcher
mailcatcher
open http://localhost:1080/
Learn more at mailcatcher. By recommendation of the library, we don't add this to our gemfile.
Running the Specs
To run all Ruby specs:
rspec
To run all the javascript specs:
yarn test
Deploying
See the Hatchbox documentation.
Conventions
Git
- Create feature branches off of
mainwith your initials and a brief description; e.g.xy-add-magic-to-orgs. - Multiple, meaningful commits with great commit messages are encouraged. Don't feel like you must squash every PR down into one.
- Push to Codeberg and open a pull request. Seek someone else to review your code.
- Feel free to commit a lot as you go and rebase against
mainto tidy up after a PR is approved.
Code Style
Standard is configured to enforce the style guide for this project.
Additional/Optional Development Details
Styling
Note: Material has moved on quite a bit since this app was created. To avoid the effort of keeping up with an external design system, this app is transitioning to a lightweight, in-house design system that is mostly (or hopefully fully) CSS with no additional frameworks. The notes below are historical, but still useful as we transition.
Two different styling systems are used within the application. The Admin experience is rendered from the server and utilizes Material Design for styling its components. The Volunteer experience is rendered from the client and utilizes Material UI for styling its components.
Server-Side Rendered Pages
Server-side rendered pages uses the simplest approach towards styling its components using Material Design. Theming is applied by overriding CSS custom properties. See _layout.scss for where we've set theming CSS custom properties.
Client-Side Rendered React
Note: We'd also like to mostly transition to sever-rendered pages everywhere, and only use dynamic pages in small quantities. They're easier to maintain, reason about, test, and they avoid the effort of keeping up with frontend framework changes. There is still plenty of react in the app so this will take a while and these notes are useful for developing in the meantime.
React State Management With Contexts
The react application uses react contexts to hold application state. We prefer to create smaller bite sized contexts scoped to particular slices of the application, and reserve the global application context to hold state that is truly global - such as the currently logged in user or theme settings. For each slice we define a dispatch and state context, which the useReducer hook produces values for. In this way we maintain the usual action -> reducer -> render data flow as seen in typical react/redux applications:
_____
| |
| API |
|_____|
^
|
_________ _____|______ __________ _________
| | | | | | | |
| actions |---->| middleware |---->| reducers |---->| context |
|_________| |____________| |__________| |_________|
^ |
| |
| __________________ |
| | | |
--------------| react components |<---------------
|__________________|
Spinning Up A Context
Creating a new context can be simplified using the useProvider custom hook. This hook takes a state and dispatch context, a reducer, and an initial state. It returns a Provider component and a connect function. Here is an example of setting up a context using the useProvider custom hook:
// domains/my-domain/context.js
import * as React from 'react';
import { defaultState, reducer } from './reducer';
import useProvider from '../../../reduxlike-contexts/useProvider';
const MyDomainStateContext = React.createContext();
const MyDomainDispatchContext = React.createContext();
const [MyDomainProvider, connectMyDomain] = useProvider(
MyDomainStateContext,
MyDomainDispatchContext,
reducer,
defaultState,
);
export { MyDomainProvider, connectMyDomain };
In order to connect to the state in this context, wrap a component tree in the resulting provider:
// domains/my-domain/AComponent.jsx
import { MyDomainProvider } from './context'
...
<MyDomainProvider>
<ChildComponent />
</MyDomainProvider>
...
You may then connect any child of this component tree:
// domains/my-domain/ChildComponent.jsx
import { connectMyDomain } from './context'
...
const ChildComponent = ({ dispatch, height, width }) => { ... }
export default connectMyDomain(ChildComponent)
...
This will inject the dispatch function and each top level value in the context's state as props.
In this example, if the context's state looks like {height: 5, width: 10}, then connectMyDomain provides the dispatch function as well as height and width as props to the wrapped component.
Actions and Async Actions
Actions work in the expected way, defining action constants (const SET_HEIGHT = 'SET_HEIGHT')) and action creator functions (const setHeight = (height) => ({ type: SET_HEIGHT, data: height })).
We also use a thunks-style middleware which allows for async actions in the form of second order functions, e.g.:
const fetchShape = (arguments) => async (dispatch) => {
dispatch(setLoading(true));
const result = await asyncMethod(arguments);
dispatch(setShape(result));
}
Any such second order function should work as long as the resulting function takes a dispatch as the only argument.
Code Coverage (local)
Coverage for the ruby specs:
$ COVERAGE=true rspec
Using chromedriver
The chromedriver version used in this project is maintained by the selenium-webdriver gem. selenium-webdriver handles downloading and installing chromedriver automatically for you and exists outside of any chromedriver previously installed on the machine.
Headed vs headless Chrome
System specs marked with js: true run using headless Chrome by default. When writing or troubleshooting specs, you may want to run the normal (i.e. "headed") version of Chrome so you can see what is being rendered and use the Chrome developer tools.
To do so, specify HEADLESS=false in your environment when running the specs. For example:
$ HEADLESS=false bin/rspec spec/system
Continuous Integration with TeamCity
This uses TeamCity for CI. The configuration is configured in the private teamcity repository.
Landing Page
The landing page is a static site built-in a different repo that we have as a submodule here. When the landing page repo is updated, we need to be sure to update it here. Do that with:
$ git submodule init
$ git submodule update
$ git submodule foreach git pull origin main
$ rake landing_page:update
If you use an IDE to git "add" it might not see the submodule update. Be sure to use the command git commit -a to ensure it finds it.
Importing Organizations from Food Rescue Robot
The rake task robot:import_region was created to help organizations transition from
Food Rescue Robot to Rootable. This task copies over a Region's users, sites, and schedules to a Rootable instance.
To import an organization, first load a database dump from Food Rescue Robot into a Postgres instance that Rootable can connect to. Make note of the DB's connection string, you'll set this as the ROBOT_DATABASE_URL environment variable when running the import.
Next, determine the Region ID of the Organization you are going to import. One way to do this is by fetching the region in a Rails console like this:
$ rails c
> Robot::Region.where("name like '%Boulder%'")
=> [#<Robot::Region id: 1, ...>, ...]
Next, choose a cutover date: this is the date after which the organization will begin using Rootable. Shifts will be scheduled starting one day after this date. You'll pass this in as CUTOVER_DATE in the yyyy-mm-dd format.
Finally, run the import:
rails robot:import_region CUTOVER_DATE=2023年01月01日 REGION_ID=1
This will take a moment, but you should see some notes on what's imported as the data is transferred.
Limitations
Due to time constraints, there are some limitations that should be noted:
- The import is not idempotent. Meaning, a new region will be created each time
robot:import_regionis run. - Pickup/dropoff logs from volunteers will not be created. Only volunteers, locations, and shifts are imported.
- Any existing Rootable users (matched by email address) will be removed from their current organization and added to the newly created organization. When an existing user is found, their organization membership and role will be updated, but additional things like name, timezone, and phone number, will not be updated. You will see a note when existing users are encountered during the import.
- Any Food Rescue Robot admins will be imported as volunteers if they are currently assigned shifts because Rootable used to not allow assigning admins to shifts.
Server Environments
Hosting
Acceptance and Production are hosted on Hatchbox under the Rootable account. You must be added to the account to access.
Environment Variables
Several common features and operational parameters can be set using environment variables.
Required for deployment
DATABASE_URL- URL of the PostgreSQL database; e.g.postgres://user:password@host:port/databaseNODE_ENV- Set toproductionfor all deployment environmentsRACK_ENV- Set toproductionfor all deployment environmentsRAILS_ENV- Set toproductionfor all deployment environmentsSECRET_KEY_BASE- Secret key base for verifying signed cookies. Should be 30+ random characters and secret!SMTP_HOST- SMTP server addressSMTP_PORT- SMTP server portSMTP_AUTHENTICATION- SMTP authentication type, "plain" or "login"SMTP_USERNAME- Username for SMTP authenticationSMTP_PASSWORD- Password for SMTP authenticationSMTP_SENDER_ADDRESS- Address from which emails will be sent; e.g.Name <user@domain.com>AWS_BUCKET_NAME- Backblaze B2 bucket name for storing attachmentsAWS_ACCESS_KEY_ID- Backblaze B2 bucket key ID for storing attachmentsAWS_SECRET_ACCESS_KEY- Backblaze B2 bucket secret for storing attachmentsCANONICAL_HOSTNAME- Canonical hostname for this application. Other incoming requests will be redirected to this hostname. Also used by mailers to generate full URLs; e.g.rootable.org
Optional
ASSET_HOST- Load assets from this host (e.g. CDN) (default: none).BASIC_AUTH_PASSWORD- Enable basic auth with this password.BASIC_AUTH_USER- Set a basic auth username (not required, password enables basic auth).CONTACT_EMAIL_ADDRESS- Address for users to reach out about administrative details. This is meant to reach the party responsible for running the app, not individual organizations.DB_POOL- Number of DB connections per pool (i.e. per worker) (default: RAILS_MAX_THREADS or 5).FORCE_SSL- Require SSL for all requests, redirecting if necessary (default: false).HATCHBOX_ACCEPTANCE_DEPLOY_KEY- key to deploy to hatchbox acceptance (local only)HATCHBOX_PRODUCTION_DEPLOY_KEY- key to deploy to hatchbox production (local only)PORT- Port to listen on (default: 3000) (local only)RACK_TIMEOUT_SERVICE_TIMEOUT- Terminate requests that take longer than this time (default: 15s).RAILS_LOG_TO_STDOUT- Log to standard out, good for Hatchbox (default: false).RAILS_MAX_THREADS- Threads per worker (default: 5).RAILS_MIN_THREADS- Threads per worker (default: 5).RAILS_SERVE_STATIC_FILES- Serve static assets (default: false).WEB_CONCURRENCY- Number of puma workers to spawn (default: 1).