Fix MICR font path, date import, PDF button bug; clean up config; add TODO markers

- Hardcode GnuMICR.otf path in pdfService.js; remove MICR_FONT_PATH env var
- Fix normalizeDate to handle MM/DD/YY (2-digit year) and return null on no match
- Fix generatePdf button DOM bug: update span directly instead of overwriting textContent
- Remove .env.example and NTFY_URL from docker-compose (app has no required config)
- Remove redundant fonts volume mount from docker-compose (fonts bundled in image)
- Mark MVP TODO items complete; add // TODO comments in source for post-MVP features
- Update README: correct slot height, remove stale env var docs
This commit is contained in:
2026-03-12 15:49:56 -06:00
parent f5b1292aff
commit c7ce87afd5
9 changed files with 54 additions and 73 deletions
-1
View File
@@ -1 +0,0 @@
MICR_FONT_PATH=/app/fonts/micrenc.ttf
+6 -40
View File
@@ -1,6 +1,6 @@
# check-printing # check-printing
Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPrinting (Halfpricesoft) — a Windows-only desktop app — with a Dockerized Node.js web app accessible on the local network. Self-hosted web app for printing checks on blank check stock. A Dockerized Node.js web app accessible on the local network.
## Stack ## Stack
@@ -11,42 +11,11 @@ Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPr
- **Frontend:** Vanilla JS, no framework - **Frontend:** Vanilla JS, no framework
- **Container:** Docker Compose pulling from Docker Hub - **Container:** Docker Compose pulling from Docker Hub
## Project structure
```
check-printing/
├── src/
│ ├── routes/
│ │ ├── checks.js # CRUD for check records
│ │ └── pdf.js # PDF generation endpoint
│ ├── services/
│ │ └── pdfService.js # PDFKit rendering, MICR line, amount-to-words
│ ├── db/
│ │ ├── schema.sql # SQLite schema
│ │ └── database.js # DB connection + WAL mode
│ └── app.js # Express app, all routes
├── migrations/
│ └── import-mdb.js # One-time .mdb → SQLite migration script
├── public/
│ ├── index.html
│ ├── css/style.css
│ └── js/app.js
├── fonts/
│ └── GnuMICR.otf
├── docker/
│ └── Dockerfile
├── docker-compose.yml
├── package.json
└── .env.example
```
## Getting started ## Getting started
### Production (Docker) ### Production (Docker)
```bash ```bash
cp .env.example .env
# Edit .env with your NTFY_URL if desired
docker compose pull docker compose pull
docker compose up -d docker compose up -d
``` ```
@@ -59,7 +28,6 @@ If you have an existing ezCheckPrinting `.mdb` file, click **Import .mdb** inste
```bash ```bash
npm install npm install
cp .env.example .env
npm run dev # nodemon src/app.js npm run dev # nodemon src/app.js
``` ```
@@ -85,7 +53,7 @@ The script imports account config (T100), logo (Settings), check layout (T200),
1. Select 13 checks from the ledger (checkbox column) 1. Select 13 checks from the ledger (checkbox column)
2. Click **Generate PDF** 2. Click **Generate PDF**
3. A 3-up 8.5"×11" PDF opens in a new tab — three 3.667" check slots per page 3. A 3-up 8.5"×11" PDF opens in a new tab — three 3.5" check slots per page
4. Print from the browser; checks are marked as printed in the ledger 4. Print from the browser; checks are marked as printed in the ledger
Use the **Reprint** button on printed checks to regenerate without re-marking them. Use the **Reprint** button on printed checks to regenerate without re-marking them.
@@ -93,8 +61,8 @@ Use the **Reprint** button on printed checks to regenerate without re-marking th
## Check layout ## Check layout
- Page: 8.5" × 11", zero margins - Page: 8.5" × 11", zero margins
- Three slots of 3.667" each - Three check slots of 3.5" each; remaining ~0.5" is tear-off strip
- MICR line at Y = 3.4" from top of slot (0.267" from bottom) - MICR line at 0.267" from bottom of each slot
- MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding) - MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding)
## CI/CD ## CI/CD
@@ -103,11 +71,9 @@ Push to `main` triggers a GitHub Actions workflow that builds a multi-arch Docke
## Environment variables ## Environment variables
See `.env.example`. Key variables: The app has no required configuration. These are set in `docker-compose.yml`:
| Variable | Default | Description | | Variable | Default | Description |
| -------- | ------- | ----------- | | -------- | ------- | ----------- |
| `PORT` | `3000` | HTTP port | | `PORT` | `3000` | HTTP port |
| `DB_PATH` | `data/check-printing.db` | SQLite database path | | `DB_PATH` | `/app/data/check-printing.db` | SQLite database path |
| `MICR_FONT_PATH` | *(see .env.example)* | Path to GnuMICR.otf inside container |
| `NTFY_URL` | — | ntfy topic URL for push notifications |
+17 -17
View File
@@ -2,23 +2,23 @@
## MVP ## MVP
- [ ] Build `public/index.html` -- app shell, header with company name and current check number - [x] Build `public/index.html` -- app shell, header with company name and current check number
- [ ] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view - [x] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view
- [ ] Build `public/js/app.js` -- all frontend logic via `fetch()` - [x] Build `public/js/app.js` -- all frontend logic via `fetch()`
- [ ] Check ledger table -- columns: check #, date, payee, amount, memo, printed status - [x] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
- [ ] Ledger: filter by printed / unprinted - [x] Ledger: filter by printed / unprinted
- [ ] Ledger: sort by check number and date - [x] Ledger: sort by check number and date
- [ ] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default - [x] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default
- [ ] New check form -- slide-in panel or modal, not a separate page - [x] New check form -- slide-in panel or modal, not a separate page
- [ ] Edit mode for unprinted checks (inline or same panel as new check form) - [x] Edit mode for unprinted checks (inline or same panel as new check form)
- [ ] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI - [x] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI
- [ ] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab - [x] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab
- [ ] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`) - [x] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`)
- [ ] Delete confirmation for unprinted checks - [x] Delete confirmation for unprinted checks
- [ ] Basic error display for API failures (failed PDF generation, validation errors) - [x] Basic error display for API failures (failed PDF generation, validation errors)
- [ ] Amount input validation -- numeric, two decimal places, greater than zero - [x] Amount input validation -- numeric, two decimal places, greater than zero
- [ ] Date input defaults to today - [x] Date input defaults to today
- [ ] Check number display -- show next check number on new check form (read from account) - [x] Check number display -- show next check number on new check form (read from account)
- [ ] Run migration against `Montana Dinosaur Center.mdb` and verify all check records import correctly - [ ] Run migration against `Montana Dinosaur Center.mdb` and verify all check records import correctly
- [ ] Verify PDF output: spot-check field positions against a printed check from the original software - [ ] Verify PDF output: spot-check field positions against a printed check from the original software
- [ ] Verify MICR line renders using GnuMICR.otf and lands at correct Y position - [ ] Verify MICR line renders using GnuMICR.otf and lands at correct Y position
+1 -4
View File
@@ -1,6 +1,6 @@
services: services:
check-printing: check-printing:
image: ${DOCKERHUB_USERNAME}/check-printing:latest image: dogiakos/check-printing:latest
container_name: check-printing container_name: check-printing
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -8,13 +8,10 @@ services:
volumes: volumes:
# Persistent data: SQLite DB lives here # Persistent data: SQLite DB lives here
- check-printing-data:/app/data - check-printing-data:/app/data
- ./fonts:/app/fonts:ro
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
- DB_PATH=/app/data/check-printing.db - DB_PATH=/app/data/check-printing.db
# Full path to MICR font inside container
- MICR_FONT_PATH=${MICR_FONT_PATH}
volumes: volumes:
check-printing-data: check-printing-data:
+9 -4
View File
@@ -119,6 +119,8 @@ function normalizeFont(fontName, isBold) {
return mapped; return mapped;
} }
// TODO: Support multi-account .mdb import -- run migration per account and associate records with account_id
// ---- Import: T100 (account config) ------------------------------------------ // ---- Import: T100 (account config) ------------------------------------------
function importAccount() { function importAccount() {
@@ -346,15 +348,18 @@ function importChecks() {
function normalizeDate(raw) { function normalizeDate(raw) {
if (!raw) return null; if (!raw) return null;
// mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS" or "YYYY-MM-DD" // mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS", "MM/DD/YY", or "YYYY-MM-DD"
const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
if (mdyMatch) { if (mdyMatch) {
const [, m, d, y] = mdyMatch; const [, m, d, y] = mdyMatch;
return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`; const year = y.length === 2
? (parseInt(y, 10) >= 50 ? '19' : '20') + y
: y;
return `${year}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
} }
const isoMatch = raw.match(/^(\d{4}-\d{2}-\d{2})/); const isoMatch = raw.match(/^(\d{4}-\d{2}-\d{2})/);
if (isoMatch) return isoMatch[1]; if (isoMatch) return isoMatch[1];
return raw; return null;
} }
// ---- Run -------------------------------------------------------------------- // ---- Run --------------------------------------------------------------------
+5 -3
View File
@@ -294,7 +294,9 @@ async function generatePdf() {
const btn = document.getElementById('btn-generate-pdf'); const btn = document.getElementById('btn-generate-pdf');
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Generating…'; const countSpan = document.getElementById('selected-count');
const savedCount = countSpan.textContent;
countSpan.textContent = '…';
try { try {
const res = await fetch('/api/pdf', { const res = await fetch('/api/pdf', {
@@ -310,9 +312,9 @@ async function generatePdf() {
window.open(URL.createObjectURL(blob), '_blank'); window.open(URL.createObjectURL(blob), '_blank');
await loadChecks(); // refresh to show printed status await loadChecks(); // refresh to show printed status
} catch (err) { } catch (err) {
countSpan.textContent = savedCount;
btn.disabled = false;
alert(`PDF error: ${err.message}`); alert(`PDF error: ${err.message}`);
} finally {
refreshPdfButton();
} }
} }
+6
View File
@@ -87,6 +87,12 @@ app.post('/api/account/setup', (req, res) => {
res.status(201).json({ success: true }); res.status(201).json({ success: true });
}); });
// TODO: Add multi-account support -- account switcher, per-account routing/logo/layout, account_id FK on checks and layout_fields
// TODO: Add basic auth or simple password gate for any network-exposed deployment
// TODO: Add deposit slip support -- deposits table, PDF generation, ledger, and slide-in entry form
// Account info endpoint (read-only for Phase 1) // Account info endpoint (read-only for Phase 1)
app.get('/api/account', (req, res) => { app.get('/api/account', (req, res) => {
const db = require('./db/database'); const db = require('./db/database');
+4
View File
@@ -4,6 +4,8 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db/database'); const db = require('../db/database');
// TODO: Add ledger reporting -- date range filter, payee search, total amount display, CSV export
// GET /api/checks - list all checks, newest first // GET /api/checks - list all checks, newest first
router.get('/', (req, res) => { router.get('/', (req, res) => {
const { after, printed } = req.query; const { after, printed } = req.query;
@@ -36,6 +38,8 @@ router.get('/:id', (req, res) => {
res.json(check); res.json(check);
}); });
// TODO: Add payee address book -- store and recall payee name + address lines, autocomplete on new check form
// POST /api/checks - create a new check // POST /api/checks - create a new check
router.post('/', (req, res) => { router.post('/', (req, res) => {
const { payee, amount, check_date, memo, note1, note2, const { payee, amount, check_date, memo, note1, note2,
+6 -4
View File
@@ -28,9 +28,7 @@ const MICR_Y_IN = SLOT_HEIGHT_IN - 0.267; // 0.267" from bottom of slot
// MICR line format: transit symbol (⑆) and on-us symbol (⑈) in E-13B encoding. // MICR line format: transit symbol (⑆) and on-us symbol (⑈) in E-13B encoding.
// The GnuMICR / micrenc font maps these to specific characters. // The GnuMICR / micrenc font maps these to specific characters.
// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈ // Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈
const MICR_FONT_PATH = process.env.MICR_FONT_PATH const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.otf');
? path.resolve(process.env.MICR_FONT_PATH)
: path.join(__dirname, '../../fonts/micrenc.ttf');
// Amount in words conversion // Amount in words conversion
function amountToWords(amount) { function amountToWords(amount) {
@@ -121,9 +119,11 @@ function generateCheckPdf(account, checks, fields) {
doc.on('end', () => resolve(Buffer.concat(buffers))); doc.on('end', () => resolve(Buffer.concat(buffers)));
doc.on('error', reject); doc.on('error', reject);
// TODO: Add 1-up with stub layout -- render Stub-prefixed fields from layout_fields alongside the check body
// Separate layout fields into check body vs stub fields // Separate layout fields into check body vs stub fields
const bodyFields = fields.filter(f => !f.field_name.startsWith('Stub')); const bodyFields = fields.filter(f => !f.field_name.startsWith('Stub'));
const stubFields = fields.filter(f => f.field_name.startsWith('Stub')); const stubFields = fields.filter(f => f.field_name.startsWith('Stub')); // eslint-disable-line no-unused-vars
// We always render 3 slots; empty slots get a blank placeholder // We always render 3 slots; empty slots get a blank placeholder
for (let slot = 0; slot < 3; slot++) { for (let slot = 0; slot < 3; slot++) {
@@ -261,6 +261,8 @@ function resolveFieldValue(fieldName, check, account) {
} }
} }
// TODO: Add visual layout editor -- UI to nudge field X/Y positions and printer offset calibration (offset_left/right/up/down)
/** /**
* Sets the PDFKit font based on a layout field's font properties. * Sets the PDFKit font based on a layout field's font properties.
* Falls back to Helvetica if the stored font name is not a built-in. * Falls back to Helvetica if the stored font name is not a built-in.