CPA-Dashboard is a Shiny application built with shiny.router and Tabler UI templates. The app currently focuses on browsing organizations, viewing an organization's profile, and selecting an organization from survey data.
- A routed Shiny app with a shared layout and page templates.
- A login page whose organization picker is populated from survey data loaded at runtime.
- An organizations list page and an organization details page.
- A small test suite that checks app startup, template rendering, and data helpers.
app.Rboots the app.R/data.Rloads the data layer from single-concern modules inR/data/(encryption, dimension metadata, survey transform pipeline, data access, interview data, translations).R/lang.Rloads the translations and language settings.R/ui.Rwires the router and top-level UI.R/server.Rhandles route-specific rendering.R/templates/contains reusable UI pieces:layout/for shared layout components like the headerpages/for full page templatescomponents/for smaller UI fragments used by pages
data/survey_data.csv.encis the source for the organization select list.
Install required R packages:
make installmake runThis starts the app on http://0.0.0.0:3838.
The live dashboard is hosted on Posit Connect Cloud, which is connected to this GitHub repository. You never have to publish the app by hand — it updates itself.
Here is the whole flow:
- You make your changes on a branch and open a pull request.
- A reviewer approves it and merges the pull request into the
mainbranch. - Posit Connect Cloud notices that
mainhas changed, rebuilds the app with your latest changes, and publishes it to the live site automatically.
Within a few minutes of merging to main, the live dashboard shows your changes.
Good to know:
- Only changes merged into
maingo live. Work on other branches is safe to experiment with and won't affect the live site. - Every pull request automatically runs the test suite first, so problems are
usually caught before anything reaches
main. - The secret data key (
CPA_DATA_KEY) lives securely inside Posit Connect Cloud, which lets the live app read the encrypted survey data. It is never stored in the code.
Connect Cloud reads a manifest.json file in the project root to know which
files and R packages the app needs. If you add, rename, or remove files (or add
a new R package), refresh that list and commit it as part of your change:
make manifestIf a deploy fails with Unable to locate manifest.json, regenerate it with the
command above and commit the result.
The app currently defines these routes:
/- home pagelogin- login pageorganizations- organization list pageorganizations/details- organization details page
The organization selector on the login page is generated from survey data.
load_survey_data()reads and decryptsdata/survey_data.csv.enc. This is the raw reader, used by the transform pipeline and the offline scripts.- For encrypted files, decryption uses the
CPA_DATA_KEYenvironment variable at runtime. load_displayable_survey_data()is the same data with non-consenting organizations removed. Every public surface reads through this one.get_org_names()extracts, trims, deduplicates, and sorts the organization names.organizations_list_ui()renders those names into the<select>control.
The survey asks each organization whether it may be shown on the public
dashboard. That answer is stored on every row as display_on_website.
Consent is strictly opt-in: an organization appears only when the column
holds an explicit Yes. A No, a blank answer, and any organization whose row
predates the question are all withheld — from the organizations list, from the
login picker, from login validation, and from the details page, so a hand-typed
?id= URL will not surface one either.
The answer is stored rather than applied at import, because the transform is
cumulative and never deletes an organization. Dropping a No row on the way in
would leave that organization's earlier consenting row in the dataset and still
on display. Keeping the flag makes a Yes -> No -> Yes change just a change
of value.
If you rebuild the dataset from an export that does not carry the question, the
dashboard lists nothing. That is intended: no consent on record is not consent.
R/scripts/transform_survey.R prints the column it matched and a Yes/No/blank
count on every run, so a mismatch shows up immediately. If it reports 0 Yes for
an export that did ask the question, the column name is wrong — add the real one
to SURVEY_DISPLAY_COL_CANDIDATES in R/data/survey_pipeline.R.
You can keep survey data encrypted in a public repository and decrypt it only at app runtime.
- Set an encryption key in your shell:
export CPA_DATA_KEY="your-strong-secret"- Encrypt a plaintext survey file:
INPUT=/path/to/survey_data.csv OUTPUT=data/survey_data.csv.enc make encrypt-dataThis creates data/survey_data.csv.enc.
- Run the app with
CPA_DATA_KEYset so it can decrypt at runtime.
Optional: decrypt locally for inspection/debugging:
INPUT=data/survey_data.csv.enc OUTPUT=/tmp/survey_data.csv make decrypt-dataNotes:
- Never commit
CPA_DATA_KEYto git. - Store
CPA_DATA_KEYin deployment secrets (for example Posit Connect, Docker/Kubernetes secrets, or a cloud secret manager).
Run the test suite with:
make testThe tests cover:
- app startup
- page and component templates
- router-driven server behavior
- survey data loading and organization name extraction
- publication-consent filtering (including a revoked-consent round trip)
- encrypted data helper and runtime decryption behavior
- The project uses
shiny.routerfor navigation. - Template files are grouped under
R/templates/layout,R/templates/pages, andR/templates/components. - If you add new routes or templates, update the relevant tests so the README and test coverage stay aligned.