🖥️ J4H Frontend Explainer

How the browser side of this app is built

J4H uses no front-end framework — no React, Vue, or Angular. The entire browser-side of the app is built with plain HTML, CSS, and JavaScript, plus three focused libraries. This keeps the app simple, fast, and easy to understand.
📄HTML
🎨CSS
Vanilla JS
🔣Jinja2
📊Chart.js
🔒Web Crypto
📱Capacitor
📄
HTML & CSS
Structure and styling — hand-written, no build step
  • Every page is a plain .html file inside the templates/ folder
  • CSS is written inline in each template inside a <style> tag — no external stylesheet
  • Layout uses CSS Flexbox and CSS Grid — no Bootstrap or Tailwind
  • The purple gradient background (#667eea → #764ba2) is shared across all pages
  • Responsive design is handled with @media (max-width: 768px) queries
Example — the gradient used on every page
body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
Why no framework? The app has a small number of pages with straightforward layouts. Plain HTML/CSS is faster to load, easier to read, and requires no build tools or dependencies.
Vanilla JavaScript
All interactivity — no jQuery, no framework
  • All API calls use the browser's built-in Fetch API — no Axios or jQuery
  • DOM manipulation uses standard methods: getElementById, innerHTML, classList
  • The passcode keypad, form submissions, entry rendering, and calendar are all pure JS
  • JS is written directly in <script> tags at the bottom of each HTML page
  • No transpiler (Babel) or bundler (Webpack/Vite) — the browser runs the code directly
Example — fetching and rendering entries
async function loadEntries() { const response = await fetch('/api/entries'); const data = await response.json(); await decryptEntries(data); renderEntries(data); }
Why vanilla JS? The interactions are straightforward — fetch data, decrypt it, display it. There is no complex state management or component tree that would justify a framework.
🔣
Jinja2 Templates
Flask's templating engine — used minimally
  • Flask uses Jinja2 to serve HTML files from the templates/ folder
  • Called via render_template('page.html') in each Flask route
  • In J4H, Jinja2 is used very lightly — mostly just to serve the pages, not to inject data
  • Most data is loaded after the page loads via JavaScript Fetch calls to the API
  • This pattern (serve static HTML, then fetch data) is called a Single Page App (SPA) style
Example — Flask route serving a template
@app.route('/entries') def entries_page(): return render_template('entries.html')
Why not render data server-side? Fetching data via JavaScript keeps the server stateless and makes it easy to add encryption — the browser can decrypt entries before displaying them, which the server cannot do.
📊
Chart.js
The one charting library — loaded from a CDN
  • Used on the Pain Trends (/chart) and Vitals (/vitals) pages
  • Loaded from a CDN: cdn.jsdelivr.net/npm/[email protected]
  • Uses scatter chart type for pain entries (each dot is one diary entry)
  • Uses line chart type for vitals (one line per vital sign over time)
  • The chartjs-adapter-date-fns plugin is also loaded to handle date axes
  • Chart data is assembled in JavaScript after entries are fetched and decrypted
Example — creating a Chart.js scatter chart
const chart = new Chart(ctx, { type: 'scatter', data: { datasets: datasets }, options: { scales: { x: { type: 'time' }, y: { min: 0, max: 10 } } } });
Why Chart.js? It is the most widely used browser charting library, has excellent documentation, and handles time-series data out of the box with the date adapter.
🔒
Web Crypto API
Built into the browser — no library needed
  • A standard browser API available in all modern browsers as window.crypto.subtle
  • Used in static/crypto.js to encrypt and decrypt diary entries
  • PBKDF2 — derives a 256-bit AES key from the passcode (100,000 iterations)
  • AES-GCM — encrypts/decrypts each diary entry with a random IV
  • The derived key is stored in sessionStorage for the browser session
  • Encrypted entries are stored as ENC:<iv>:<ciphertext> in the database
  • Requires HTTPScrypto.subtle is unavailable over plain HTTP
Example — deriving a key from the passcode
const key = await crypto.subtle.deriveKey({ name: 'PBKDF2', salt: enc.encode('j4h-diary-v1'), iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
Why client-side encryption? The server never sees plaintext diary entries. Even if the database were compromised, the entries would be unreadable without the passcode.
📱
Capacitor
Wraps the web app into a native Android app
  • Capacitor is a tool by Ionic that wraps a web app in a native mobile shell
  • On Android, it opens the app URL in a WebView — essentially a browser inside an app
  • Configured in capacitor.config.json with the server URL set to https://j4h.org
  • The android/ folder is the full Android Studio project generated by Capacitor
  • No changes to the web code are needed — the same HTML/CSS/JS runs in both the browser and the app
  • App ID: com.j4h.healthdiary, built and deployed via Android Studio
capacitor.config.json
{ "appId": "com.j4h.healthdiary", "appName": "J4H Health Diary", "server": { "url": "https://j4h.org", "androidScheme": "https" } }
Why Capacitor instead of React Native? The app is already a web app. Capacitor lets us reuse 100% of the existing code as a mobile app with zero rewriting. React Native would require rewriting the entire frontend in React.

The big picture

Flask serves plain HTML pages. The browser loads them, then JavaScript fetches data from the Flask API. Entries are decrypted in the browser using the Web Crypto API before being displayed. Chart.js renders the visualizations. Capacitor packages the whole thing as an Android app. No build step. No framework. Just the web platform.