Compare commits

..
14 Commits
Author SHA1 Message Date
randy fc59cf595e Add a friendly 502/503/504 page (nginx serves it directly, no backend needed)
deploy / deploy (push) Successful in 9s
2026-09-15 22:31:08 +02:00
randy 7ea03e5bc5 Rename /labels route to /repository
deploy / deploy (push) Successful in 9s
2026-09-15 22:29:10 +02:00
randy 61d190ed91 Move behind nginx reverse proxy: app binds 127.0.0.1:4567, nginx owns :80
deploy / deploy (push) Successful in 11s
Avoids the cap_net_bind_service dance breaking on every RVM ruby patch
upgrade (it happened today after today's ruby-4.0.7 install).
2026-09-15 22:24:40 +02:00
randy 46fd345b7f Match klikatko/mc-panel look: dark navbar, Bootstrap 5.3.8, MDI icons
deploy / deploy (push) Successful in 12s
2026-09-15 22:17:49 +02:00
randy 4308850a2e Test page: replace inverted section banners with plain '=' rules, frame page with '-'
deploy / deploy (push) Successful in 10s
Matches the style settled on for the print-issues receipt -- '=' for
labeled section breaks, '-' for the plain top/bottom frame, no more
fully blacked-out rows.
2026-09-13 12:40:00 +02:00
randy a58d4b66ea Update README: Epson routes, API usage example with the home-gitops print-issues workflow
deploy / deploy (push) Successful in 9s
2026-09-13 12:35:24 +02:00
randy ec9278c0dd Remove doubled-up padding on the divider links, add explicit flex align to the brand row
deploy / deploy (push) Successful in 9s
ps-3 stacked on top of nav-link's own padding made the gap after the
border look uneven -- nav-link's built-in padding is enough on its own.
2026-09-13 12:20:45 +02:00
randy f351212395 Fix navbar dividers: border-start on the link itself instead of a standalone vr div
deploy / deploy (push) Successful in 10s
The vr utility needs stretch alignment from its flex parent to size
correctly -- inside a plain nav-item li it collapsed/misaligned instead
of showing as a clean vertical rule.
2026-09-13 12:18:00 +02:00
randy eafea9f728 Add visual separators: inverted banner sections on receipts, dividers in navbar
deploy / deploy (push) Successful in 9s
ReceiptBuilder#invert (GS B) + #section_separator give the test page (and
future receipts) a full-width white-on-black banner between sections --
the closest a direct-thermal printer gets to a second color. Navbar gets
a vr divider between Home | Brother/Shipping labels/Receipts | Labels to
group the three logical clusters.
2026-09-13 12:16:09 +02:00
randy f8857384fd Shorten default barcode height -- printer default (~162 dots) prints noticeably tall
deploy / deploy (push) Successful in 10s
2026-09-13 12:12:28 +02:00
randy d2900361b3 Add Epson test page: text sizes, EAN-13, QR, and a raster image
deploy / deploy (push) Successful in 9s
ReceiptBuilder#image implements GS v 0 raster printing (no image library
needed for the demo -- a procedural 8px checkerboard proves it end to
end). Skipped attempting the printer's real-time clock: unsure the
TM-T20II actually has a battery-backed RTC (that's usually an option on
higher-end models), not worth guessing at an undocumented command.
2026-09-13 12:09:52 +02:00
randy 790d716d57 Fix ESC/POS notation parser: '#' only starts a comment as its own token
deploy / deploy (push) Successful in 9s
Stripping from the first '#' anywhere in the line ate content like '#5
Fix thing' (an issue reference) entirely, since it starts with '#' with
no preceding whitespace. Comments now only trigger on a standalone '#'
token (i.e. whitespace on both sides), matching the documented examples.
2026-09-13 11:51:36 +02:00
randy f09c92f201 Restore plain print (no save) as the default for /shipping/zpl and /epson/raw
deploy / deploy (push) Successful in 9s
Requiring a name broke the simple 'just print this' API/CI use case --
now that only applies to explicit save/save_and_print, an omitted commit
param (or any other value) just prints, matching how these endpoints
behaved before the Save/Save & Print buttons were added.
2026-09-13 11:47:07 +02:00
randy 368bf670b9 Revert "Print open home-gitops issues on the Epson receipt printer"
This reverts commit b44985b75f.
2026-09-13 11:46:08 +02:00
13 changed files with 259 additions and 107 deletions

No files matched your search

-1
View File
@@ -1,5 +1,4 @@
.session_secret .session_secret
.gitea_token
public/files/* public/files/*
!public/files/.gitkeep !public/files/.gitkeep
data/labels/* data/labels/*
+31 -6
View File
@@ -1,18 +1,43 @@
= labelprint = labelprint
Unified UI for printing labels/stickers: Unified UI for printing labels/receipts:
* Native ZPL printing to a Datamax-ONeil thermal barcode printer * Native ZPL printing to a Datamax-ONeil thermal barcode printer
* A proxy/switcher in front of three https://github.com/DL6ER/brother_ql_web[brother_ql_web] instances, one per Brother QL printer * A proxy/switcher in front of three https://github.com/DL6ER/brother_ql_web[brother_ql_web] instances, one per Brother QL printer
* ESC/POS receipt printing to an Epson TM-T20II
== Routes == Routes
* `/` -- upload a PDF and print it (ZPL) * `/` -- home page, links to everything below
* `/upload` -- upload form * `/upload`, `POST /upload` -- upload a PDF/ZPL file and print it (Datamax)
* `/test-print` -- print the bundled test label * `POST /shipping/zpl` -- print raw ZPL text (see "API usage" below)
* `/test-print` -- print the bundled Datamax test label
* `/epson` -- receipt printing UI (title/body/QR template, or raw ESC/POS mnemonic notation)
* `POST /epson/print` -- print a title/body/QR receipt
* `POST /epson/raw` -- print already-formatted ESC/POS mnemonic notation (see "API usage" below)
* `POST /epson/test-print` -- print an Epson capability test page (text sizes, barcode, QR, raster image)
* `/labels` -- shared repository of saved ZPL labels / ESC-POS receipts (list, edit, preview, print, delete)
* `/brother` -- list of Brother QL printers * `/brother` -- list of Brother QL printers
* `/brother/<printer>/*` -- proxied to that printer's brother_ql_web instance (UI and API alike) * `/brother/<printer>/*` -- proxied to that printer's brother_ql_web instance (UI and API alike)
== API usage (e.g. from Gitea Actions)
`POST /shipping/zpl` and `POST /epson/raw` are plain, unauthenticated form-POST endpoints -- no session/login needed, and printing without a `save_as`/`commit` param just prints (nothing gets kept in the label repository). That makes labelprint usable as a "dumb" print API for other automation: it doesn't need to know anything about the caller, and the caller doesn't need any labelprint-specific client, just an HTTP form POST.
Example: `randy/home-gitops` has a workflow (`.gitea/workflows/print-issues.yaml` + `scripts/print_issues.rb`) that fetches its own open issues via the Gitea API and prints a summary on the Epson receipt printer -- the Gitea-specific logic (fetching, formatting) lives entirely in that script, not here:
[source,ruby]
----
require 'net/http'
res = Net::HTTP.post_form(
URI('http://rpi-printserver.hv.priesol.net/epson/raw'),
'content' => "ESC E 0x01\nHello from CI\nESC E 0x00\nLF"
)
----
`content` uses labelprint's mnemonic ESC/POS notation: `ESC`/`GS`/`FS`/`LF`/`CR`/`HT`/`NUL` as command names, `0xNN` as a raw byte, anything else as literal text (CP852-encoded) -- see the `/epson` page for the full reference and worked examples (alignment, bold, barcodes). `POST /shipping/zpl` works the same way but with a `zpl` form field taking plain ZPL instead.
== Running == Running
[source,shell] [source,shell]
@@ -25,9 +50,9 @@ Requires `config.json` (see printer definitions inside) and a `.session_secret`
== Deployment == Deployment
Runs as `sinatra.service` (systemd) on `rpi-printserver.local`, under RVM (`rvm default do bundle exec ruby app.rb` -- always uses whatever ruby is currently the RVM default, no manual bumping needed). Runs as `sinatra.service` (systemd) on `rpi-printserver.hv.priesol.net`, under RVM (`rvm default do bundle exec ruby app.rb` -- always uses whatever ruby is currently the RVM default, no manual bumping needed).
`.gitea/workflows/deploy.yaml` deploys on every push to `main`: SSH into `rpi-printserver.local`, `git pull`, `bundle install`, restart the service. `.gitea/workflows/deploy.yaml` deploys on every push to `main`: SSH into `rpi-printserver.hv.priesol.net`, `git pull`, `bundle install`, restart the service.
== Dependency updates == Dependency updates
+121 -71
View File
@@ -3,14 +3,15 @@ require 'sinatra'
require "sinatra/reloader" require "sinatra/reloader"
require 'tmpdir' require 'tmpdir'
require 'net/http' require 'net/http'
require 'json'
require_relative 'lib/receipt_builder' require_relative 'lib/receipt_builder'
require_relative 'lib/label_repository' require_relative 'lib/label_repository'
set :protection, :except => :frame_options set :protection, :except => :frame_options
set :environment, :production set :environment, :production
set :bind, "::" # nginx reverse-proxies :80 -> here. Loopback-only and an unprivileged port
set :port, 80 # means no more cap_net_bind_service dance on every RVM ruby upgrade.
set :bind, "127.0.0.1"
set :port, 4567
if !File.exist?('config.json') || !File.readable?('config.json') if !File.exist?('config.json') || !File.readable?('config.json')
raise RuntimeException, 'Could not read config.json' raise RuntimeException, 'Could not read config.json'
@@ -108,6 +109,56 @@ helpers do
receipt.print!(PRINTER_DEVICE_EPSON) receipt.print!(PRINTER_DEVICE_EPSON)
end end
# Procedural 64x64 checkerboard (8px squares) -- no image library needed,
# just to prove GS v 0 raster printing works end to end.
def checkerboard_bitmap(size_px = 64, block_px = 8)
width_bytes = size_px / 8
bytes = "".b
size_px.times do |y|
row_block = y / block_px
width_bytes.times do |xb|
byte = 0
8.times do |bit|
x = xb * 8 + bit
col_block = x / block_px
byte |= (1 << (7 - bit)) if (row_block + col_block).even?
end
bytes << byte.chr
end
end
bytes
end
# Demonstrates most of what the printer can do: text sizes/styles, EAN-13,
# QR code, and a raster image -- mirrors /test-print's role for the Datamax.
def print_epson_test_page
receipt = ReceiptBuilder.new
receipt.align(:center).bold.size(:big).text("TM-T20II TEST\n").size(:normal).bold(false)
receipt.align(:left).text(('-' * 48) + "\n")
receipt.section_separator('TEXT STYLES')
receipt.text("Normal text\n")
receipt.bold.text("Bold text\n").bold(false)
receipt.underline.text("Underlined text\n").underline(false)
receipt.size(:wide).text("Double width\n").size(:normal)
receipt.size(:high).text("Double height\n").size(:normal)
receipt.size(:big).text("Double both\n").size(:normal)
receipt.section_separator('BARCODE')
receipt.align(:center).ean13('2012345678902')
receipt.section_separator('QR CODE')
receipt.align(:center).qr('https://git.priesol.net/randy/home-gitops')
receipt.section_separator('IMAGE')
receipt.align(:center).image(64, 64, checkerboard_bitmap)
receipt.align(:left).text(('-' * 48) + "\n")
receipt.text("Printed: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
receipt.feed(3).cut
receipt.print!(PRINTER_DEVICE_EPSON)
end
ESCPOS_MNEMONICS = { ESCPOS_MNEMONICS = {
'ESC' => "\x1B", 'GS' => "\x1D", 'FS' => "\x1C", 'DLE' => "\x10", 'ESC' => "\x1B", 'GS' => "\x1D", 'FS' => "\x1C", 'DLE' => "\x10",
'LF' => "\x0A", 'CR' => "\x0D", 'HT' => "\x09", 'NUL' => "\x00" 'LF' => "\x0A", 'CR' => "\x0D", 'HT' => "\x09", 'NUL' => "\x00"
@@ -118,17 +169,23 @@ helpers do
# Hello # literal text # Hello # literal text
# LF # LF
# GS k 0x03 12345678 0x00 # GS k 0x03 12345678 0x00
# Each `#...` trailing comment is stripped, then each line's # A trailing comment is only a comment when "#" is its own
# whitespace-separated tokens become either a known control mnemonic, a # whitespace-separated token -- not just stripped from the first "#"
# raw `0xNN` byte, or literal text (CP852-encoded) -- consecutive literal # anywhere in the line, which would mangle literal text like "#5 Fix
# tokens are rejoined with a single space so plain prose still reads # thing" (an issue reference, not a comment). Each remaining token
# correctly, while whitespace around control/byte tokens is dropped since # becomes a known control mnemonic, a raw `0xNN` byte, or literal text
# those are just for human readability, not printer output. # (CP852-encoded) -- consecutive literal tokens are rejoined with a
# single space so plain prose still reads correctly, while whitespace
# around control/byte tokens is dropped since those are just for human
# readability, not printer output.
def parse_escpos_notation(text) def parse_escpos_notation(text)
bytes = "".b bytes = "".b
text.each_line do |line| text.each_line do |line|
prev_was_text = false prev_was_text = false
line.sub(/#.*/, '').split.each do |token| tokens = line.split
comment_at = tokens.index('#')
tokens = tokens[0...comment_at] if comment_at
tokens.each do |token|
if ESCPOS_MNEMONICS.key?(token) if ESCPOS_MNEMONICS.key?(token)
bytes << ESCPOS_MNEMONICS[token] bytes << ESCPOS_MNEMONICS[token]
prev_was_text = false prev_was_text = false
@@ -155,45 +212,6 @@ helpers do
receipt.print!(PRINTER_DEVICE_EPSON) receipt.print!(PRINTER_DEVICE_EPSON)
end end
GITEA_TOKEN_FILE = File.join(File.dirname(__FILE__), '.gitea_token')
def fetch_open_issues(repo)
token = File.read(GITEA_TOKEN_FILE).strip
uri = URI("https://git.priesol.net/api/v1/repos/randy/#{repo}/issues?state=open&type=issues&limit=50")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "token #{token}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
raise "Gitea API returned #{res.code}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)
end
# Prints a receipt-style summary of a repo's open issues -- number, title,
# labels, one separator per issue, total at the end.
def print_issues(repo)
issues = fetch_open_issues(repo).sort_by { |i| i['number'] }
receipt = ReceiptBuilder.new
receipt.align(:center).bold.text("Open issues: #{repo}\n").bold(false)
receipt.text("#{Time.now.strftime('%Y-%m-%d %H:%M')}\n")
receipt.align(:left).text(('-' * 32) + "\n")
if issues.empty?
receipt.text("(no open issues)\n")
else
issues.each do |issue|
receipt.bold.text("##{issue['number']} #{issue['title']}\n").bold(false)
labels = issue['labels'].map { |l| l['name'] }.join(', ')
receipt.text("#{labels}\n") unless labels.empty?
receipt.text(('-' * 32) + "\n")
end
end
receipt.text("Total: #{issues.size}\n")
receipt.feed(3).cut
receipt.print!(PRINTER_DEVICE_EPSON)
end
# Renders ZPL to a PNG via Labelary's public API for an inline preview. # Renders ZPL to a PNG via Labelary's public API for an inline preview.
# Fixed at the Datamax label size (4x6in @ 8dpmm ~= 203dpi) -- good enough # Fixed at the Datamax label size (4x6in @ 8dpmm ~= 203dpi) -- good enough
# for a thumbnail, not meant to be exact for other sizes. # for a thumbnail, not meant to be exact for other sizes.
@@ -253,16 +271,6 @@ get '/' do
erb :index erb :index
end end
post '/issues/print' do
begin
print_issues('home-gitops')
flash 'Open issues sent to printer', 'success'
rescue => e
flash "Failed to print issues (#{e.message})", 'danger'
end
redirect '/'
end
get '/upload' do get '/upload' do
erb :upload erb :upload
end end
@@ -270,9 +278,15 @@ end
post '/shipping/zpl' do post '/shipping/zpl' do
zpl = params[:zpl].to_s zpl = params[:zpl].to_s
name = params[:save_as].to_s name = params[:save_as].to_s
if zpl.strip.empty? if zpl.strip.empty?
flash 'ZPL text is empty', 'warning' flash 'ZPL text is empty', 'warning'
elsif name.empty? redirect '/upload'
end
case params[:commit]
when 'save', 'save_and_print'
if name.empty?
flash 'Name is required to save', 'warning' flash 'Name is required to save', 'warning'
else else
LABEL_REPO.save(name: name, type: 'zpl', content: zpl) LABEL_REPO.save(name: name, type: 'zpl', content: zpl)
@@ -287,6 +301,16 @@ post '/shipping/zpl' do
flash "Saved '#{name}'", 'success' flash "Saved '#{name}'", 'success'
end end
end end
else
# No explicit save/save_and_print action -- e.g. a plain API/CI call.
# labelprint stays a "dumb" print API here: just print, nothing saved.
begin
print_to_datamax(zpl)
flash 'ZPL sent to printer', 'success'
rescue => e
flash "ZPL print failed (#{e.message})", 'danger'
end
end
redirect '/upload' redirect '/upload'
end end
@@ -341,6 +365,16 @@ get '/epson' do
erb :epson erb :epson
end end
post '/epson/test-print' do
begin
print_epson_test_page
flash 'Test page sent to Epson printer', 'success'
rescue => e
flash "Epson test print failed (#{e.message})", 'danger'
end
redirect '/epson'
end
post '/epson/print' do post '/epson/print' do
begin begin
print_receipt(title: params[:title].to_s, body: params[:body].to_s, qr: params[:qr].to_s) print_receipt(title: params[:title].to_s, body: params[:body].to_s, qr: params[:qr].to_s)
@@ -354,9 +388,15 @@ end
post '/epson/raw' do post '/epson/raw' do
content = params[:content].to_s content = params[:content].to_s
name = params[:save_as].to_s name = params[:save_as].to_s
if content.strip.empty? if content.strip.empty?
flash 'Receipt text is empty', 'warning' flash 'Receipt text is empty', 'warning'
elsif name.empty? redirect '/epson'
end
case params[:commit]
when 'save', 'save_and_print'
if name.empty?
flash 'Name is required to save', 'warning' flash 'Name is required to save', 'warning'
else else
LABEL_REPO.save(name: name, type: 'escpos', content: content) LABEL_REPO.save(name: name, type: 'escpos', content: content)
@@ -371,30 +411,40 @@ post '/epson/raw' do
flash "Saved '#{name}'", 'success' flash "Saved '#{name}'", 'success'
end end
end end
else
# No explicit save/save_and_print action -- e.g. a plain API/CI call.
# labelprint stays a "dumb" print API here: just print, nothing saved.
begin
print_raw_escpos(content)
flash 'Sent to Epson printer', 'success'
rescue => e
flash "Epson print failed (#{e.message})", 'danger'
end
end
redirect '/epson' redirect '/epson'
end end
get '/labels' do get '/repository' do
@labels = LABEL_REPO.all @labels = LABEL_REPO.all
erb :labels erb :repository
end end
get '/labels/:id/edit' do get '/repository/:id/edit' do
@label = LABEL_REPO.find(params[:id]) @label = LABEL_REPO.find(params[:id])
halt 404, 'unknown label' unless @label halt 404, 'unknown label' unless @label
erb :label_edit erb :label_edit
end end
post '/labels/:id' do post '/repository/:id' do
label = LABEL_REPO.update(params[:id], name: params[:name].to_s, content: params[:content].to_s) label = LABEL_REPO.update(params[:id], name: params[:name].to_s, content: params[:content].to_s)
halt 404, 'unknown label' unless label halt 404, 'unknown label' unless label
flash "Saved '#{label['name']}'", 'success' flash "Saved '#{label['name']}'", 'success'
redirect '/labels' redirect '/repository'
end end
get '/labels/:id/preview.png' do get '/repository/:id/preview.png' do
label = LABEL_REPO.find(params[:id]) label = LABEL_REPO.find(params[:id])
halt 404 unless label && label['type'] == 'zpl' halt 404 unless label && label['type'] == 'zpl'
@@ -405,7 +455,7 @@ get '/labels/:id/preview.png' do
png png
end end
post '/labels/:id/print' do post '/repository/:id/print' do
label = LABEL_REPO.find(params[:id]) label = LABEL_REPO.find(params[:id])
halt 404, 'unknown label' unless label halt 404, 'unknown label' unless label
@@ -420,13 +470,13 @@ post '/labels/:id/print' do
rescue => e rescue => e
flash "Print failed (#{e.message})", 'danger' flash "Print failed (#{e.message})", 'danger'
end end
redirect '/labels' redirect '/repository'
end end
post '/labels/:id/delete' do post '/repository/:id/delete' do
LABEL_REPO.delete(params[:id]) LABEL_REPO.delete(params[:id])
flash 'Label deleted', 'success' flash 'Label deleted', 'success'
redirect '/labels' redirect '/repository'
end end
get '/brother' do get '/brother' do
+36 -2
View File
@@ -65,6 +65,28 @@ class ReceiptBuilder
self self
end end
# White-on-black reverse video (GS B n) -- available if a receipt ever
# wants a real highlight, but plain "=" rules read better for routine
# section breaks than a fully blacked-out line.
def invert(enable = true)
@buffer << (enable ? "\x1D\x42\x01" : "\x1D\x42\x00")
self
end
# A single "=" rule with an optional label centered in it, e.g.
# section_separator("SECTION") -> "========= SECTION =========".
def section_separator(label = '', width: 48)
line = if label.empty?
'=' * width
else
mid = " #{label} "
pad = [width - mid.length, 0].max
left = pad / 2
('=' * left) + mid + ('=' * (pad - left))
end
feed(1).text("#{line}\n").feed(1)
end
def size(mode) def size(mode)
@buffer << case mode @buffer << case mode
when :big then TXT_BIG when :big then TXT_BIG
@@ -87,12 +109,24 @@ class ReceiptBuilder
self self
end end
# Native ESC/POS EAN-13 # Native ESC/POS EAN-13. height is in dots (GS h) -- the printer's default
def ean13(code) # (~162 dots) prints noticeably tall; 50 is a shorter, still-scannable size.
def ean13(code, height: 50)
@buffer << "\x1D\x68" << height.chr # GS h n -- set barcode height
@buffer << "\x1D\x6B\x02" << code.b << "\x00" @buffer << "\x1D\x6B\x02" << code.b << "\x00"
self self
end end
# Raster bit image (GS v 0) -- bytes must already be packed 1bpp,
# row-major, (width_px/8 rounded up) bytes per row, MSB-first, 1=black.
def image(width_px, height_px, bytes)
width_bytes = (width_px + 7) / 8
@buffer << "\x1D\x76\x30\x00"
@buffer << [width_bytes % 256, width_bytes / 256, height_px % 256, height_px / 256].pack('C4')
@buffer << bytes
self
end
def feed(lines = 3) def feed(lines = 3)
@buffer << ("\n" * lines) @buffer << ("\n" * lines)
self self
+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="sk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="5">
<title>Print server</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
<style>
body { background: #212529; color: #adb5bd; }
.mdi-printer-off { font-size: 4rem; color: #6c757d; }
</style>
</head>
<body class="d-flex align-items-center justify-content-center vh-100">
<div class="text-center">
<i class="mdi mdi-printer-off"></i>
<h1 class="mt-3">Tlačiareň spí</h1>
<p class="text-muted">Print server práve nabieha alebo sa reštartuje.<br>Stránka sa sama skúsi znova o chvíľu...</p>
<div class="spinner-border text-secondary mt-2" role="status"></div>
</div>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
# Installed at /etc/nginx/sites-available/labelprint on rpi-printserver
# (symlinked into sites-enabled), not auto-deployed -- kept here just so a
# reflash/rebuild isn't a guessing game. nginx owns :80 so the app itself
# never needs cap_net_bind_service (which a plain RVM ruby patch upgrade
# silently drops, since it's tied to the old binary's path).
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name rpi-printserver.hv.priesol.net;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:4567;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 502 503 504 /502.html;
location = /502.html {
root /etc/nginx/error_pages;
internal;
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid"> <div class="container-fluid d-flex align-items-center">
<a class="navbar-brand" href="/">Print server</a> <a class="navbar-brand" href="/"><i class="mdi mdi-printer"></i> Print server</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@@ -10,7 +10,7 @@
<a class="nav-link <%= 'active' if request.path == '/' %>" href="/">Home</a> <a class="nav-link <%= 'active' if request.path == '/' %>" href="/">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <%= 'active' if request.path.start_with?('/brother') %>" href="/brother">Brother</a> <a class="nav-link border-start <%= 'active' if request.path.start_with?('/brother') %>" href="/brother">Brother</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <%= 'active' if request.path == '/upload' %>" href="/upload">Shipping labels</a> <a class="nav-link <%= 'active' if request.path == '/upload' %>" href="/upload">Shipping labels</a>
@@ -19,7 +19,7 @@
<a class="nav-link <%= 'active' if request.path == '/epson' %>" href="/epson">Receipts</a> <a class="nav-link <%= 'active' if request.path == '/epson' %>" href="/epson">Receipts</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <%= 'active' if request.path.start_with?('/labels') %>" href="/labels">Labels</a> <a class="nav-link border-start <%= 'active' if request.path.start_with?('/repository') %>" href="/repository">Labels</a>
</li> </li>
</ul> </ul>
<% if request.path.start_with?('/brother') %> <% if request.path.start_with?('/brother') %>
+5 -1
View File
@@ -15,6 +15,10 @@
<button class="btn btn-secondary" type="submit">Print</button> <button class="btn btn-secondary" type="submit">Print</button>
</form> </form>
<form action="/epson/test-print" method="POST" class="mt-3">
<button class="btn btn-outline-secondary" type="submit">Print test page (sizes, barcode, QR, image)</button>
</form>
<hr class="mt-4"> <hr class="mt-4">
<h5>Free text (ESC/POS mnemonic notation)</h5> <h5>Free text (ESC/POS mnemonic notation)</h5>
@@ -47,4 +51,4 @@ GS V 0x00 # Full cut</code></pre>
</div> </div>
</form> </form>
<p class="mt-4"><a href="/labels">See saved receipts &rarr;</a></p> <p class="mt-4"><a href="/repository">See saved receipts &rarr;</a></p>
-10
View File
@@ -9,13 +9,3 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">home-gitops open issues</h5>
<p class="card-text">Print a receipt-style summary of currently open issues.</p>
<form action="/issues/print" method="POST">
<button class="btn btn-secondary" type="submit">Print open issues</button>
</form>
</div>
</div>
+2 -2
View File
@@ -6,7 +6,7 @@
<span class="badge bg-info text-dark">ESC/POS &middot; Epson</span> <span class="badge bg-info text-dark">ESC/POS &middot; Epson</span>
<% end %> <% end %>
<form action="/labels/<%= @label['id'] %>" method="POST" class="mt-3"> <form action="/repository/<%= @label['id'] %>" method="POST" class="mt-3">
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input name="name" class="form-control" type="text" id="name" value="<%= @label['name'] %>" /> <input name="name" class="form-control" type="text" id="name" value="<%= @label['name'] %>" />
@@ -16,5 +16,5 @@
<textarea name="content" class="form-control" id="content" rows="12" style="font-family: monospace;"><%= @label['content'] %></textarea> <textarea name="content" class="form-control" id="content" rows="12" style="font-family: monospace;"><%= @label['content'] %></textarea>
</div> </div>
<button class="btn btn-secondary" type="submit">Save</button> <button class="btn btn-secondary" type="submit">Save</button>
<a href="/labels" class="btn btn-outline-secondary">Cancel</a> <a href="/repository" class="btn btn-outline-secondary">Cancel</a>
</form> </form>
+5 -4
View File
@@ -5,12 +5,13 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAOzs7ADl5eUA/Pz8ACsZBgD+/v4AAwMDAPv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWZmZmZmIxERZmZmZmZmERFmZmZmZmYREWZmZoZmZhERZmYUVmZmERFmYREWZmYREWYRERFmZhERZhYREXZmERFmZmYRZmYREWZmZhFmZhERZmZmERFmERFmZmYHEWYREWZmZmZmZhERZmZmZmZmERFmZmZmZmYREWZmZmZmZhHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" /> <link href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD9/f0AAAAAAOzs7ADl5eUA/Pz8ACsZBgD+/v4AAwMDAPv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWZmZmZmIxERZmZmZmZmERFmZmZmZmYREWZmZoZmZhERZmYUVmZmERFmYREWZmYREWYRERFmZhERZhYREXZmERFmZmYRZmYREWZmZhFmZhERZmZmERFmERFmZmYHEWYREWZmZmZmZhERZmZmZmZmERFmZmZmZmYREWZmZmZmZhHAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAAwAMAAMADAADAAwAA" rel="icon" type="image/x-icon" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css" /> <link href="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>
<div class="container">
<%= erb :_navbar, layout: false %> <%= erb :_navbar, layout: false %>
<main class="container-fluid px-4">
<div class="row"> <div class="row">
<% if @alert_success then %> <% if @alert_success then %>
<div class="alert alert-success"><%=@alert_success%></div> <div class="alert alert-success"><%=@alert_success%></div>
@@ -23,8 +24,8 @@
<% end %> <% end %>
<%= yield %> <%= yield %>
</div> </div>
</div> </main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script> <script src="https://unpkg.com/dropzone@6.0.0-beta.1/dist/dropzone-min.js"></script>
<script> <script>
Dropzone.options.UploadForm = { Dropzone.options.UploadForm = {
+4 -4
View File
@@ -16,11 +16,11 @@
<strong class="ms-2"><%= label['name'] %></strong> <strong class="ms-2"><%= label['name'] %></strong>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<form action="/labels/<%= label['id'] %>/print" method="POST"> <form action="/repository/<%= label['id'] %>/print" method="POST">
<button class="btn btn-sm btn-secondary" type="submit">Print</button> <button class="btn btn-sm btn-secondary" type="submit">Print</button>
</form> </form>
<a href="/labels/<%= label['id'] %>/edit" class="btn btn-sm btn-outline-secondary">Edit</a> <a href="/repository/<%= label['id'] %>/edit" class="btn btn-sm btn-outline-secondary">Edit</a>
<form action="/labels/<%= label['id'] %>/delete" method="POST"> <form action="/repository/<%= label['id'] %>/delete" method="POST">
<button class="btn btn-sm btn-outline-danger" type="submit">Delete</button> <button class="btn btn-sm btn-outline-danger" type="submit">Delete</button>
</form> </form>
</div> </div>
@@ -28,7 +28,7 @@
<div class="mt-2"> <div class="mt-2">
<% if label['type'] == 'zpl' %> <% if label['type'] == 'zpl' %>
<img src="/labels/<%= label['id'] %>/preview.png" alt="preview" style="max-height: 150px;" onerror="this.remove()" /> <img src="/repository/<%= label['id'] %>/preview.png" alt="preview" style="max-height: 150px;" onerror="this.remove()" />
<% else %> <% else %>
<pre class="bg-light p-2 mb-0" style="max-height: 150px; overflow: auto;"><%= label['content'] %></pre> <pre class="bg-light p-2 mb-0" style="max-height: 150px; overflow: auto;"><%= label['content'] %></pre>
<% end %> <% end %>
+1 -1
View File
@@ -33,4 +33,4 @@
</div> </div>
</form> </form>
<p class="mt-4"><a href="/labels">See saved labels &rarr;</a></p> <p class="mt-4"><a href="/repository">See saved labels &rarr;</a></p>