Added Printer Spoofer for testing purposes

This commit is contained in:
2026-04-29 16:45:09 +03:00
parent bb39088464
commit 240abb2e73
7 changed files with 138 additions and 5 deletions

View File

@@ -16,6 +16,7 @@ VALID_SETTINGS = {
"business_day.force_close_allowed": "Allow force-closing business day with open tables", "business_day.force_close_allowed": "Allow force-closing business day with open tables",
"system.timezone": "IANA timezone name used by the backend container (e.g. Europe/Athens). Requires container restart to take effect.", "system.timezone": "IANA timezone name used by the backend container (e.g. Europe/Athens). Requires container restart to take effect.",
"ui.table_colours": "JSON blob of table card colour scheme (light + dark modes) for the Waiter PWA.", "ui.table_colours": "JSON blob of table card colour scheme (light + dark modes) for the Waiter PWA.",
"dev.spoof_printing": "When enabled, all print jobs are silently dropped. Devices behave as if printing succeeded.",
} }
DEFAULTS = { DEFAULTS = {
@@ -24,6 +25,7 @@ DEFAULTS = {
"business_day.force_close_allowed": "true", "business_day.force_close_allowed": "true",
"system.timezone": "Europe/Athens", "system.timezone": "Europe/Athens",
"ui.table_colours": "", "ui.table_colours": "",
"dev.spoof_printing": "false",
} }

View File

@@ -20,6 +20,7 @@ from database import SessionLocal
from models.order import Order, OrderItem, PrintLog from models.order import Order, OrderItem, PrintLog
from models.printer import Printer from models.printer import Printer
from models.product import Product from models.product import Product
from models.settings import PosSettings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,7 +74,19 @@ def check_printer(ip: str, port: int) -> bool:
return False return False
def is_spoof_mode() -> bool:
"""Stateless check — opens its own DB session. For use outside route_and_print."""
db = SessionLocal()
try:
return _is_spoof_mode(db)
finally:
db.close()
def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]: def send_test_print(ip: str, port: int, name: str) -> Tuple[bool, str]:
if is_spoof_mode():
logger.info("Spoof printing ON — dropping test print for %s", name)
return True, ""
try: try:
p = _get_printer(ip, port) p = _get_printer(ip, port)
p._raw(b'\x1b\x61\x01') p._raw(b'\x1b\x61\x01')
@@ -164,6 +177,9 @@ def _print_kitchen_ticket(p: Network, order: Order, items: List[OrderItem], db:
def print_waiter_report(ip: str, port: int, report: dict, mode: str): def print_waiter_report(ip: str, port: int, report: dict, mode: str):
"""Print a waiter shift/period report. mode='simple'|'extensive'.""" """Print a waiter shift/period report. mode='simple'|'extensive'."""
if is_spoof_mode():
logger.info("Spoof printing ON — dropping waiter report print")
return
try: try:
p = _get_printer(ip, port) p = _get_printer(ip, port)
@@ -222,6 +238,9 @@ def print_waiter_report(ip: str, port: int, report: dict, mode: str):
def print_printer_report(ip: str, port: int, report: dict, mode: str): def print_printer_report(ip: str, port: int, report: dict, mode: str):
"""Print a per-printer totals report. mode='simple'|'extensive'.""" """Print a per-printer totals report. mode='simple'|'extensive'."""
if is_spoof_mode():
logger.info("Spoof printing ON — dropping printer report print")
return
try: try:
p = _get_printer(ip, port) p = _get_printer(ip, port)
@@ -282,6 +301,9 @@ def print_printer_report(ip: str, port: int, report: dict, mode: str):
def print_order_receipt(ip: str, port: int, receipt: dict): def print_order_receipt(ip: str, port: int, receipt: dict):
"""Print a manager-triggered order receipt.""" """Print a manager-triggered order receipt."""
if is_spoof_mode():
logger.info("Spoof printing ON — dropping order receipt print")
return
try: try:
p = _get_printer(ip, port) p = _get_printer(ip, port)
@@ -329,6 +351,9 @@ def print_order_receipt(ip: str, port: int, receipt: dict):
def print_order_synopsis(ip: str, port: int, synopsis: dict): def print_order_synopsis(ip: str, port: int, synopsis: dict):
"""Print a waiter-triggered order synopsis (not a kitchen ticket).""" """Print a waiter-triggered order synopsis (not a kitchen ticket)."""
if is_spoof_mode():
logger.info("Spoof printing ON — dropping order synopsis print")
return
try: try:
p = _get_printer(ip, port) p = _get_printer(ip, port)
@@ -408,7 +433,21 @@ def route_and_print_sync(order_id: int, item_ids: List[int], db: Session) -> Lis
return _do_route_and_print(order_id, item_ids, db) return _do_route_and_print(order_id, item_ids, db)
def _is_spoof_mode(db: Session) -> bool:
row = db.query(PosSettings).filter(PosSettings.key == "dev.spoof_printing").first()
return row is not None and row.value == "true"
def _do_route_and_print(order_id: int, item_ids: List[int], db: Session) -> List[dict]: def _do_route_and_print(order_id: int, item_ids: List[int], db: Session) -> List[dict]:
if _is_spoof_mode(db):
logger.info("Spoof printing ON — dropping print job for order %s", order_id)
for item_id in item_ids:
item = db.query(OrderItem).filter(OrderItem.id == item_id).first()
if item:
item.printed = True
db.commit()
return [{"printer_name": "spoof", "success": True, "error": None}]
results = [] results = []
order = db.query(Order).filter(Order.id == order_id).first() order = db.query(Order).filter(Order.id == order_id).first()

View File

@@ -1,10 +1,12 @@
import { useState } from 'react' import { useState } from 'react'
import AppInfoTab from './tabs/AppInfoTab' import AppInfoTab from './tabs/AppInfoTab'
import ColoursTab from './tabs/ColoursTab' import ColoursTab from './tabs/ColoursTab'
import DevelopmentTab from './tabs/DevelopmentTab'
const TABS = [ const TABS = [
{ key: 'app-info', label: 'App Info' }, { key: 'app-info', label: 'App Info' },
{ key: 'colours', label: 'UI Personalization' }, { key: 'colours', label: 'UI Personalization' },
{ key: 'development', label: 'Development' },
] ]
export default function SettingsPage() { export default function SettingsPage() {
@@ -44,8 +46,9 @@ export default function SettingsPage() {
</div> </div>
{/* Tab content */} {/* Tab content */}
{activeTab === 'app-info' && <AppInfoTab />} {activeTab === 'app-info' && <AppInfoTab />}
{activeTab === 'colours' && <ColoursTab />} {activeTab === 'colours' && <ColoursTab />}
{activeTab === 'development' && <DevelopmentTab />}
</div> </div>
) )
} }

View File

@@ -0,0 +1,86 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import client from '../../../api/client'
function Toggle({ checked, onChange, disabled }) {
return (
<button
role="switch"
aria-checked={checked}
onClick={() => !disabled && onChange(!checked)}
style={{
width: 44, height: 24, borderRadius: 999, border: 'none', cursor: disabled ? 'not-allowed' : 'pointer',
background: checked ? '#dc2626' : '#d1d5db',
position: 'relative', transition: 'background 150ms', flexShrink: 0, opacity: disabled ? 0.5 : 1,
}}
>
<span style={{
position: 'absolute', top: 3, left: checked ? 23 : 3,
width: 18, height: 18, borderRadius: '50%', background: 'white',
transition: 'left 150ms', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
}} />
</button>
)
}
export default function DevelopmentTab() {
const qc = useQueryClient()
const { data: settings, isLoading } = useQuery({
queryKey: ['settings'],
queryFn: () => client.get('/api/settings/').then(r => r.data),
})
const mutation = useMutation({
mutationFn: ({ key, value }) => client.put(`/api/settings/${key}`, { value }),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['settings'] }) },
onError: () => toast.error('Failed to update setting'),
})
const spoofOn = settings?.['dev.spoof_printing']?.value === 'true'
function toggleSpoof(val) {
mutation.mutate({ key: 'dev.spoof_printing', value: val ? 'true' : 'false' })
toast.success(val ? 'Spoof printing ON — printers are silenced' : 'Spoof printing OFF — printers active')
}
if (isLoading) return <p style={{ color: '#6b7280', fontSize: 14 }}>Loading</p>
return (
<div style={{ maxWidth: 560 }}>
<div style={{
background: '#fef2f2', border: '1px solid #fca5a5',
borderRadius: 10, padding: '12px 16px', marginBottom: 24,
fontSize: 13, color: '#991b1b',
}}>
These settings are intended for testing only. Do not leave them enabled in production.
</div>
<div style={{
background: 'white', border: '1px solid #e5e7eb',
borderRadius: 10, padding: '16px 20px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16,
}}>
<div>
<div style={{ fontWeight: 600, fontSize: 14, color: '#111827' }}>
Spoof Printer Mode
</div>
<div style={{ fontSize: 13, color: '#6b7280', marginTop: 3 }}>
All print jobs are silently dropped. Devices behave as if printing succeeded no errors, nothing printed.
</div>
</div>
<Toggle checked={spoofOn} onChange={toggleSpoof} disabled={mutation.isPending} />
</div>
{spoofOn && (
<div style={{
marginTop: 12, padding: '10px 14px',
background: '#fff7ed', border: '1px solid #fed7aa',
borderRadius: 8, fontSize: 13, color: '#92400e', fontWeight: 500,
}}>
Spoof mode is active printers are silenced.
</div>
)}
</div>
)
}

BIN
simple-pos-system.zip Normal file

Binary file not shown.

View File

@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.7tvu7c24jlg" "revision": "0.qb5i81hq8"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({ export default defineConfig({
server: {
allowedHosts: 'all',
},
plugins: [ plugins: [
react(), react(),
VitePWA({ VitePWA({