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
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
@@ -11,42 +11,11 @@ Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPr
- **Frontend:** Vanilla JS, no framework
- **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
### Production (Docker)
```bash
cp .env.example .env
# Edit .env with your NTFY_URL if desired
docker compose pull
docker compose up -d
```
@@ -59,7 +28,6 @@ If you have an existing ezCheckPrinting `.mdb` file, click **Import .mdb** inste
```bash
npm install
cp .env.example .env
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)
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
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
- Page: 8.5" × 11", zero margins
- Three slots of 3.667" each
- MICR line at Y = 3.4" from top of slot (0.267" from bottom)
- Three check slots of 3.5" each; remaining ~0.5" is tear-off strip
- MICR line at 0.267" from bottom of each slot
- MICR format: `A{routing}A {account}C {checkNo}A` (GnuMICR E-13B encoding)
## CI/CD
@@ -103,11 +71,9 @@ Push to `main` triggers a GitHub Actions workflow that builds a multi-arch Docke
## Environment variables
See `.env.example`. Key variables:
The app has no required configuration. These are set in `docker-compose.yml`:
| Variable | Default | Description |
| -------- | ------- | ----------- |
| `PORT` | `3000` | HTTP port |
| `DB_PATH` | `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 |
| `DB_PATH` | `/app/data/check-printing.db` | SQLite database path |
+17 -17
View File
@@ -2,23 +2,23 @@
## MVP
- [ ] 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
- [ ] Build `public/js/app.js` -- all frontend logic via `fetch()`
- [ ] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
- [ ] Ledger: filter by printed / unprinted
- [ ] Ledger: sort by check number and date
- [ ] 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
- [ ] 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
- [ ] "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`)
- [ ] Delete confirmation for unprinted checks
- [ ] Basic error display for API failures (failed PDF generation, validation errors)
- [ ] Amount input validation -- numeric, two decimal places, greater than zero
- [ ] Date input defaults to today
- [ ] Check number display -- show next check number on new check form (read from account)
- [x] Build `public/index.html` -- app shell, header with company name and current check number
- [x] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view
- [x] Build `public/js/app.js` -- all frontend logic via `fetch()`
- [x] Check ledger table -- columns: check #, date, payee, amount, memo, printed status
- [x] Ledger: filter by printed / unprinted
- [x] Ledger: sort by check number and date
- [x] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default
- [x] New check form -- slide-in panel or modal, not a separate page
- [x] Edit mode for unprinted checks (inline or same panel as new check form)
- [x] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI
- [x] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab
- [x] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`)
- [x] Delete confirmation for unprinted checks
- [x] Basic error display for API failures (failed PDF generation, validation errors)
- [x] Amount input validation -- numeric, two decimal places, greater than zero
- [x] Date input defaults to today
- [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
- [ ] 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
+1 -4
View File
@@ -1,6 +1,6 @@
services:
check-printing:
image: ${DOCKERHUB_USERNAME}/check-printing:latest
image: dogiakos/check-printing:latest
container_name: check-printing
restart: unless-stopped
ports:
@@ -8,13 +8,10 @@ services:
volumes:
# Persistent data: SQLite DB lives here
- check-printing-data:/app/data
- ./fonts:/app/fonts:ro
environment:
- NODE_ENV=production
- PORT=3000
- DB_PATH=/app/data/check-printing.db
# Full path to MICR font inside container
- MICR_FONT_PATH=${MICR_FONT_PATH}
volumes:
check-printing-data:
+9 -4
View File
@@ -119,6 +119,8 @@ function normalizeFont(fontName, isBold) {
return mapped;
}
// TODO: Support multi-account .mdb import -- run migration per account and associate records with account_id
// ---- Import: T100 (account config) ------------------------------------------
function importAccount() {
@@ -346,15 +348,18 @@ function importChecks() {
function normalizeDate(raw) {
if (!raw) return null;
// mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS" or "YYYY-MM-DD"
const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
// 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{2,4})/);
if (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})/);
if (isoMatch) return isoMatch[1];
return raw;
return null;
}
// ---- Run --------------------------------------------------------------------
+5 -3
View File
@@ -294,7 +294,9 @@ async function generatePdf() {
const btn = document.getElementById('btn-generate-pdf');
btn.disabled = true;
btn.textContent = 'Generating…';
const countSpan = document.getElementById('selected-count');
const savedCount = countSpan.textContent;
countSpan.textContent = '…';
try {
const res = await fetch('/api/pdf', {
@@ -310,9 +312,9 @@ async function generatePdf() {
window.open(URL.createObjectURL(blob), '_blank');
await loadChecks(); // refresh to show printed status
} catch (err) {
countSpan.textContent = savedCount;
btn.disabled = false;
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 });
});
// 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)
app.get('/api/account', (req, res) => {
const db = require('./db/database');
+4
View File
@@ -4,6 +4,8 @@ const express = require('express');
const router = express.Router();
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
router.get('/', (req, res) => {
const { after, printed } = req.query;
@@ -36,6 +38,8 @@ router.get('/:id', (req, res) => {
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
router.post('/', (req, res) => {
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.
// The GnuMICR / micrenc font maps these to specific characters.
// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈
const MICR_FONT_PATH = process.env.MICR_FONT_PATH
? path.resolve(process.env.MICR_FONT_PATH)
: path.join(__dirname, '../../fonts/micrenc.ttf');
const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.otf');
// Amount in words conversion
function amountToWords(amount) {
@@ -121,9 +119,11 @@ function generateCheckPdf(account, checks, fields) {
doc.on('end', () => resolve(Buffer.concat(buffers)));
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
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
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.
* Falls back to Helvetica if the stored font name is not a built-in.