Redesign: clear active nav state, Shipping labels + Receipts get free-text/raw print, shared label repository
deploy / deploy (push) Successful in 10s
deploy / deploy (push) Successful in 10s
- Home page is now just the print server overview, no more unrelated network links (3D printers, Cockpit) left over from when this was a general landing page. - Navbar shows which section is active; Brother's per-printer buttons now live under a dedicated Brother nav item instead of always-visible. - Shipping labels: paste raw ZPL directly (no need to save to a file first), with a Labelary preview link alongside it. - Receipts: paste already-formatted ESC/POS text directly, printed as-is. - Both can save what they just printed into a shared label repository (lib/label_repository.rb, one JSON file per label under data/labels/) for reprinting later -- saving happens independently of whether the print itself succeeds, so a failed/offline printer doesn't lose the content.
This commit is contained in:
1 parent
afae57aff4
commit
66e2e7a5c0
8 files changed
+220
-47
No files matched your search
@@ -1,3 +1,5 @@
|
||||
.session_secret
|
||||
public/files/*
|
||||
!public/files/.gitkeep
|
||||
data/labels/*
|
||||
!data/labels/.gitkeep
|
||||
@@ -4,6 +4,7 @@ require "sinatra/reloader"
|
||||
require 'tmpdir'
|
||||
require 'net/http'
|
||||
require_relative 'lib/receipt_builder'
|
||||
require_relative 'lib/label_repository'
|
||||
|
||||
set :protection, :except => :frame_options
|
||||
set :environment, :production
|
||||
@@ -24,6 +25,8 @@ TEST_LABEL_PDF = File.join(File.dirname(__FILE__), 'test-label.pdf')
|
||||
|
||||
PRINTER_DEVICE_EPSON = '/dev/usb/lp_epson' # see /etc/udev/rules.d/99-epson.rules
|
||||
|
||||
LABEL_REPO = LabelRepository.new(File.join(File.dirname(__FILE__), 'data', 'labels'))
|
||||
|
||||
BROTHER_PRINTERS = {
|
||||
'ql560' => { port: 8560, label: 'Brother QL-560' },
|
||||
'ql700' => { port: 8700, label: 'Brother QL-700' },
|
||||
@@ -104,6 +107,16 @@ helpers do
|
||||
receipt.print!(PRINTER_DEVICE_EPSON)
|
||||
end
|
||||
|
||||
# Prints already-formatted ESC/POS text as-is (just the reset + code page
|
||||
# prefix and a trailing cut) -- for content prepared elsewhere or reprinted
|
||||
# from the label repository, unlike print_receipt's title/body template.
|
||||
def print_raw_escpos(content)
|
||||
receipt = ReceiptBuilder.new
|
||||
receipt.align(:left).text(content)
|
||||
receipt.feed(3).cut
|
||||
receipt.print!(PRINTER_DEVICE_EPSON)
|
||||
end
|
||||
|
||||
# Transparently reverse-proxies the current request to one of the
|
||||
# brother_ql_web instances, so all three printers (web UI and API alike)
|
||||
# are reachable through this single sinatra port instead of their own
|
||||
@@ -153,9 +166,26 @@ get '/' do
|
||||
end
|
||||
|
||||
get '/upload' do
|
||||
@labels = LABEL_REPO.all(type: 'zpl')
|
||||
erb :upload
|
||||
end
|
||||
|
||||
post '/shipping/zpl' do
|
||||
zpl = params[:zpl].to_s
|
||||
if zpl.strip.empty?
|
||||
flash 'ZPL text is empty', 'warning'
|
||||
else
|
||||
LABEL_REPO.save(name: params[:save_as], type: 'zpl', content: zpl) unless params[:save_as].to_s.empty?
|
||||
begin
|
||||
print_to_datamax(zpl)
|
||||
flash 'ZPL sent to printer', 'success'
|
||||
rescue => e
|
||||
flash "ZPL print failed (#{e.message})", 'danger'
|
||||
end
|
||||
end
|
||||
redirect '/upload'
|
||||
end
|
||||
|
||||
post '/upload' do
|
||||
if params[:file]
|
||||
|
||||
@@ -204,6 +234,7 @@ post '/test-print' do
|
||||
end
|
||||
|
||||
get '/epson' do
|
||||
@labels = LABEL_REPO.all(type: 'escpos')
|
||||
erb :epson
|
||||
end
|
||||
|
||||
@@ -217,6 +248,49 @@ post '/epson/print' do
|
||||
redirect '/epson'
|
||||
end
|
||||
|
||||
post '/epson/raw' do
|
||||
content = params[:content].to_s
|
||||
if content.strip.empty?
|
||||
flash 'Receipt text is empty', 'warning'
|
||||
else
|
||||
LABEL_REPO.save(name: params[:save_as], type: 'escpos', content: content) unless params[:save_as].to_s.empty?
|
||||
begin
|
||||
print_raw_escpos(content)
|
||||
flash 'Sent to Epson printer', 'success'
|
||||
rescue => e
|
||||
flash "Epson print failed (#{e.message})", 'danger'
|
||||
end
|
||||
end
|
||||
redirect '/epson'
|
||||
end
|
||||
|
||||
post '/labels/:id/print' do
|
||||
label = LABEL_REPO.find(params[:id])
|
||||
halt 404, 'unknown label' unless label
|
||||
|
||||
begin
|
||||
case label['type']
|
||||
when 'zpl'
|
||||
print_to_datamax(label['content'])
|
||||
when 'escpos'
|
||||
print_raw_escpos(label['content'])
|
||||
end
|
||||
flash "Printed '#{label['name']}'", 'success'
|
||||
rescue => e
|
||||
flash "Print failed (#{e.message})", 'danger'
|
||||
end
|
||||
redirect_target = label['type'] == 'zpl' ? '/upload' : '/epson'
|
||||
redirect redirect_target
|
||||
end
|
||||
|
||||
post '/labels/:id/delete' do
|
||||
label = LABEL_REPO.find(params[:id])
|
||||
redirect_target = label && label['type'] == 'zpl' ? '/upload' : '/epson'
|
||||
LABEL_REPO.delete(params[:id])
|
||||
flash 'Label deleted', 'success'
|
||||
redirect redirect_target
|
||||
end
|
||||
|
||||
get '/brother' do
|
||||
@printers = BROTHER_PRINTERS
|
||||
erb :brother_picker
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,46 @@
|
||||
require 'json'
|
||||
require 'securerandom'
|
||||
require 'fileutils'
|
||||
|
||||
# Simple file-backed store for reusable ZPL/ESC-POS label templates -- one
|
||||
# JSON file per label under DIR, similar in spirit to brother_ql_web's own
|
||||
# label repository. No per-printer targeting yet (type just says which print
|
||||
# path to use); that can be added once labels can target any of the five
|
||||
# printers via the Brother API.
|
||||
class LabelRepository
|
||||
def initialize(dir)
|
||||
@dir = dir
|
||||
FileUtils.mkdir_p(@dir)
|
||||
end
|
||||
|
||||
# type: "zpl" or "escpos"
|
||||
def all(type: nil)
|
||||
Dir.glob(File.join(@dir, '*.json')).map { |f| JSON.parse(File.read(f)) }
|
||||
.select { |l| type.nil? || l['type'] == type }
|
||||
.sort_by { |l| l['name'].downcase }
|
||||
end
|
||||
|
||||
def find(id)
|
||||
path = path_for(id)
|
||||
return nil unless File.exist?(path)
|
||||
|
||||
JSON.parse(File.read(path))
|
||||
end
|
||||
|
||||
def save(name:, type:, content:)
|
||||
id = SecureRandom.uuid
|
||||
label = { 'id' => id, 'name' => name, 'type' => type, 'content' => content }
|
||||
File.write(path_for(id), JSON.generate(label))
|
||||
label
|
||||
end
|
||||
|
||||
def delete(id)
|
||||
File.delete(path_for(id)) if File.exist?(path_for(id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def path_for(id)
|
||||
File.join(@dir, "#{id}.json")
|
||||
end
|
||||
end
|
||||
+9
-4
@@ -7,21 +7,26 @@
|
||||
<div class="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
<a class="nav-link <%= 'active' if request.path == '/' %>" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/upload">Shipping labels</a>
|
||||
<a class="nav-link <%= 'active' if request.path.start_with?('/brother') %>" href="/brother">Brother</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/epson">Receipts</a>
|
||||
<a class="nav-link <%= 'active' if request.path == '/upload' %>" href="/upload">Shipping labels</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= 'active' if request.path == '/epson' %>" href="/epson">Receipts</a>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="navbar-text me-2">Brother:</span>
|
||||
<% if request.path.start_with?('/brother') %>
|
||||
<span class="navbar-text me-2">Printer:</span>
|
||||
<div class="d-flex gap-1">
|
||||
<% BROTHER_PRINTERS.each do |key, info| %>
|
||||
<a href="/brother/<%= key %>/" class="btn btn-sm <%= request.path.start_with?("/brother/#{key}/") ? 'btn-primary' : 'btn-outline-secondary' %>"><%= info[:label].sub('Brother ', '') %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,3 +1,4 @@
|
||||
<h5>Formatted receipt</h5>
|
||||
<form action="/epson/print" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title (bold, centered)</label>
|
||||
@@ -13,3 +14,42 @@
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="submit">Print</button>
|
||||
</form>
|
||||
|
||||
<hr class="mt-4">
|
||||
|
||||
<h5>Free text (already-formatted ESC/POS)</h5>
|
||||
<p>Sent as-is (just reset + code page prefix and a trailing cut), no title/body template applied.</p>
|
||||
<form action="/epson/raw" method="POST">
|
||||
<div class="mb-3">
|
||||
<textarea name="content" class="form-control" rows="8" style="font-family: monospace;"></textarea>
|
||||
</div>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label for="save_as_escpos" class="form-label">Save as (optional)</label>
|
||||
<input name="save_as" class="form-control" type="text" id="save_as_escpos" placeholder="receipt name" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" type="submit">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% if @labels && !@labels.empty? %>
|
||||
<hr class="mt-4">
|
||||
<h5>Saved receipts</h5>
|
||||
<div class="list-group">
|
||||
<% @labels.each do |label| %>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<%= label['name'] %>
|
||||
<div class="d-flex gap-2">
|
||||
<form action="/labels/<%= label['id'] %>/print" method="POST">
|
||||
<button class="btn btn-sm btn-secondary" type="submit">Print</button>
|
||||
</form>
|
||||
<form action="/labels/<%= label['id'] %>/delete" method="POST">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
+7
-43
@@ -1,47 +1,11 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Brother Label Designer</h5>
|
||||
<p class="card-text">A web service to print labels on Brother QL label printers.</p>
|
||||
<a href="http://rpi-printserver.local:8560/labeldesigner" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">QL-560</a>
|
||||
<a href="http://rpi-printserver.local:8700/labeldesigner" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">QL-700</a>
|
||||
<a href="http://rpi-printserver.local:9100/labeldesigner" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">QL-1100</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" >
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Cockpit</h5>
|
||||
<p class="card-text">Cockpit is a web-based graphical interface for servers, intended for everyone</p>
|
||||
<a href="https://rpi-printserver.local:9090" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Go</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" >
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">ZPL Viewer</h5>
|
||||
<p class="card-text">An useful tool for labels in ZPL format.</p>
|
||||
<a href="https://labelary.com/viewer.html" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Go</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" >
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Creality Sonic Pad</h5>
|
||||
<p class="card-text">3D Printer</p>
|
||||
<a href="http://spad-9088.local" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Fluidd</a>
|
||||
<a href="http://spad-9088.local:8819" class="btn btn-outline-secondary" target="_blank" rel="noopener noreferrer">Mainsail</a>
|
||||
<a href="http://spad-9088.local:7125" class="btn btn-outline-secondary" target="_blank" rel="noopener noreferrer">Moonraker</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" >
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Bigtreetech Pad 7 CNC</h5>
|
||||
<p class="card-text">CNC</p>
|
||||
<a href="http://btt-pad7-cnc.local" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Mainsail</a>
|
||||
<a href="http://btt-pad7-cnc.local:7125" class="btn btn-outline-secondary" target="_blank" rel="noopener noreferrer">Moonraker</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" >
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Bigtreetech Pad 7 CAN</h5>
|
||||
<p class="card-text">CAN</p>
|
||||
<a href="http://rpi-btt-pad7-can.local" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Mainsail</a>
|
||||
<h5 class="card-title">Print server</h5>
|
||||
<p class="card-text">Connected printers:</p>
|
||||
<ul>
|
||||
<li>Datamax-ONeil E4204B Mark III (ZPL, 100x150) -- <a href="/upload">Shipping labels</a></li>
|
||||
<li>Epson TM-T20II (ESC/POS) -- <a href="/epson">Receipts</a></li>
|
||||
<li>Brother QL-560, QL-700, QL-1100 -- <a href="/brother">Brother</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9,3 +9,45 @@
|
||||
<form action="/test-print" method="POST" class="mt-3">
|
||||
<button class="btn btn-outline-secondary" type="submit">Print test label</button>
|
||||
</form>
|
||||
|
||||
<hr class="mt-4">
|
||||
|
||||
<h5>Free text ZPL</h5>
|
||||
<p>
|
||||
<a href="https://labelary.com/viewer.html" target="_blank" rel="noopener noreferrer">Labelary ZPL viewer</a>
|
||||
(preview/build ZPL before pasting it here)
|
||||
</p>
|
||||
<form action="/shipping/zpl" method="POST">
|
||||
<div class="mb-3">
|
||||
<textarea name="zpl" class="form-control" rows="8" placeholder="^XA...^XZ" style="font-family: monospace;"></textarea>
|
||||
</div>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label for="save_as_zpl" class="form-label">Save as (optional)</label>
|
||||
<input name="save_as" class="form-control" type="text" id="save_as_zpl" placeholder="label name" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-secondary" type="submit">Print</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<% if @labels && !@labels.empty? %>
|
||||
<hr class="mt-4">
|
||||
<h5>Saved labels</h5>
|
||||
<div class="list-group">
|
||||
<% @labels.each do |label| %>
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<%= label['name'] %>
|
||||
<div class="d-flex gap-2">
|
||||
<form action="/labels/<%= label['id'] %>/print" method="POST">
|
||||
<button class="btn btn-sm btn-secondary" type="submit">Print</button>
|
||||
</form>
|
||||
<form action="/labels/<%= label['id'] %>/delete" method="POST">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
Reference in new issue
Block a user